qwenproxy-cli 1.1.0 → 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,7 +5,11 @@
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";
@@ -22,7 +26,11 @@ interface ChatMessage {
22
26
  cachedReasoningBox?: string[];
23
27
  cachedWidth?: number;
24
28
  }
25
- 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
+ } {
26
34
  const lower = modelId.toLowerCase();
27
35
  if (
28
36
  lower.includes("image") ||
@@ -30,12 +38,35 @@ export function classifyModel(modelId: string): { badge: string; category: strin
30
38
  lower.includes("t2i") ||
31
39
  lower.includes("i2i")
32
40
  ) {
33
- 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
+ };
34
46
  }
35
47
  if (lower.includes("video") || lower.includes("t2v") || lower.includes("i2v")) {
36
- 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
+ };
37
53
  }
38
- return { badge: theme.cyan("[Texto] "), category: "Texto & Raciocínio" };
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
+ };
64
+ }
65
+ return {
66
+ badge: theme.cyan("[Texto] "),
67
+ category: "Texto & Raciocínio",
68
+ supportsReasoning: true,
69
+ };
39
70
  }
40
71
 
41
72
  export class ChatView implements TuiView {
@@ -43,14 +74,7 @@ export class ChatView implements TuiView {
43
74
  public readonly title = "Chat";
44
75
  public readonly tabNumber = 2;
45
76
 
46
- private availableModels = [
47
- "qwen3.8-max",
48
- "qwen3.7-plus",
49
- "qwen3.7-max",
50
- "z-image-turbo",
51
- "qwen-image-3.0-pro",
52
- "wan3.0-video",
53
- ];
77
+ private availableModels = [...DEFAULT_FALLBACK_MODELS];
54
78
  private selectedModelIndex = 0;
55
79
  private messages: ChatMessage[] = [];
56
80
  private inputBuffer = "";
@@ -254,7 +278,7 @@ export class ChatView implements TuiView {
254
278
  },
255
279
  });
256
280
  const info = classifyModel(chosen);
257
- if (info.category === "Texto & Raciocínio") {
281
+ if (info.supportsReasoning) {
258
282
  this.isEffortModalOpen = true;
259
283
  const effIdx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
260
284
  this.effortSelectedIndex = effIdx !== -1 ? effIdx : 0;
@@ -317,8 +341,9 @@ export class ChatView implements TuiView {
317
341
  if (this.isEffortModalOpen) {
318
342
  if (key.name === "hover" && key.mouse) {
319
343
  const { row } = key.mouse;
320
- if (row >= 9 && row < 9 + this.availableEfforts.length) {
321
- const hoverIdx = row - 9;
344
+ const startRow = 12;
345
+ if (row >= startRow && row < startRow + this.availableEfforts.length) {
346
+ const hoverIdx = row - startRow;
322
347
  if (this.effortSelectedIndex !== hoverIdx) {
323
348
  this.effortSelectedIndex = hoverIdx;
324
349
  this.onNeedsRender?.();
@@ -328,8 +353,10 @@ export class ChatView implements TuiView {
328
353
  }
329
354
  if (key.name === "click" && key.mouse) {
330
355
  const { row } = key.mouse;
331
- if (row >= 9 && row < 9 + this.availableEfforts.length) {
332
- 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;
333
360
  this.isEffortModalOpen = false;
334
361
  const currentM = this.availableModels[this.selectedModelIndex];
335
362
  saveTuiSettings({
@@ -338,7 +365,7 @@ export class ChatView implements TuiView {
338
365
  effort: this.selectedEffort,
339
366
  },
340
367
  });
341
- this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[row - 9].label}`;
368
+ this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[chosenIdx].label}`;
342
369
  this.onNeedsRender?.();
343
370
  return true;
344
371
  }
@@ -386,7 +413,7 @@ export class ChatView implements TuiView {
386
413
  if (this.isModeModalOpen) {
387
414
  if (key.name === "hover" && key.mouse) {
388
415
  const { row } = key.mouse;
389
- const startRow = 11;
416
+ const startRow = 12;
390
417
  if (row >= startRow && row < startRow + this.availableModes.length) {
391
418
  const hoverIdx = row - startRow;
392
419
  if (this.modeSelectedIndex !== hoverIdx) {
@@ -398,9 +425,10 @@ export class ChatView implements TuiView {
398
425
  }
399
426
  if (key.name === "click" && key.mouse) {
400
427
  const { row } = key.mouse;
401
- const startRow = 11;
428
+ const startRow = 12;
402
429
  if (row >= startRow && row < startRow + this.availableModes.length) {
403
- this.selectedChatMode = this.availableModes[row - startRow].id;
430
+ const chosenIdx = row - startRow;
431
+ this.selectedChatMode = this.availableModes[chosenIdx].id;
404
432
  this.isModeModalOpen = false;
405
433
  setRuntimeChatMode(this.selectedChatMode);
406
434
  saveTuiSettings({
@@ -490,6 +518,7 @@ export class ChatView implements TuiView {
490
518
  ) {
491
519
  const col = key.mouse.col;
492
520
  if (this.modelBtnStartCol > 0 && col >= this.modelBtnStartCol - 1 && col <= this.modelBtnEndCol + 1) {
521
+ void this.refreshModels();
493
522
  this.isModelModalOpen = true;
494
523
  this.modalSelectedIndex = this.selectedModelIndex;
495
524
  this.hoveredHeaderBtn = null;
@@ -530,7 +559,7 @@ export class ChatView implements TuiView {
530
559
  if (key.name === "f3") {
531
560
  const currentM = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
532
561
  const info = classifyModel(currentM);
533
- if (info.category === "Texto & Raciocínio") {
562
+ if (info.supportsReasoning) {
534
563
  this.isEffortModalOpen = true;
535
564
  const idx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
536
565
  this.effortSelectedIndex = idx !== -1 ? idx : 0;
@@ -810,7 +839,7 @@ export class ChatView implements TuiView {
810
839
  .map((m) => ({ role: m.role, content: m.content }));
811
840
 
812
841
  try {
813
- const isReasoning = classifyModel(model).category === "Texto & Raciocínio";
842
+ const isReasoning = classifyModel(model).supportsReasoning;
814
843
  const result = await streamChatCompletions({
815
844
  model,
816
845
  reasoning_effort: isReasoning ? this.selectedEffort : undefined,
@@ -887,7 +916,7 @@ export class ChatView implements TuiView {
887
916
  const currentModel = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
888
917
  const currentInfo = classifyModel(currentModel);
889
918
  const totalModels = this.availableModels.length;
890
- const isReasoning = currentInfo.category === "Texto & Raciocínio";
919
+ const isReasoning = currentInfo.supportsReasoning;
891
920
 
892
921
  const modelLabel = `[ ${currentModel} ]`;
893
922
  const styledModel = this.hoveredHeaderBtn === "model"
@@ -1066,47 +1095,42 @@ export class ChatView implements TuiView {
1066
1095
  }
1067
1096
  }
1068
1097
  for (const msg of this.messages) {
1069
- chatContent.push("");
1070
1098
  if (msg.role === "user") {
1071
- const userLines = msg.content.split(/\r?\n/);
1072
- for (let u = 0; u < userLines.length; u++) {
1073
- if (u === 0) {
1074
- chatContent.push(` ${theme.blue(glyphs.pointer + " Você:")} ${theme.white(userLines[u])}`);
1075
- } else {
1076
- chatContent.push(` ${theme.white(userLines[u])}`);
1077
- }
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
+ );
1078
1111
  }
1112
+
1113
+ // Bottom padding inside user card
1114
+ chatContent.push(` ${theme.cyan("▌")}${theme.bgUserCard(" ".repeat(cardW))}`);
1115
+ chatContent.push("");
1079
1116
  } else {
1080
1117
  const messageModel = msg.model || currentModel;
1081
- chatContent.push(` ${theme.green(glyphs.bullet + " Qwen (" + messageModel + "):")}`);
1082
- // 1. Dedicated Thinking (Reasoning) Container - Opaque, Dimmed, and Cached
1118
+
1119
+ // 1. OpenCode-style Thinking (Reasoning): Clean, indented, dimmed and unboxed
1083
1120
  if (msg.reasoning && msg.reasoning.trim().length > 0) {
1084
- const thinkWidth = Math.max(20, innerChatW - 4);
1085
- let thinkLines: string[];
1086
- if (msg.cachedWidth === innerChatW && msg.cachedReasoningBox) {
1087
- thinkLines = msg.cachedReasoningBox;
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] || "⠋";
1124
+
1125
+ if (isStillThinking) {
1126
+ chatContent.push(` ${theme.yellow(`🧠 ${spinner} Raciocinando...`)}`);
1088
1127
  } else {
1089
- const rLines = formatReasoning(msg.reasoning, thinkWidth - 4).map((l) => ` ${l}`);
1090
- if (this.isGenerating && !msg.content && this.messages.indexOf(msg) === this.messages.length - 1) {
1091
- const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
1092
- rLines.push("");
1093
- rLines.push(` ${theme.yellow(`${spinner} Raciocinando...`)}`);
1094
- }
1095
- thinkLines = drawBox({
1096
- title: "🧠 Raciocínio",
1097
- width: thinkWidth,
1098
- borderColor: theme.borderInactive,
1099
- titleColor: theme.muted,
1100
- content: rLines,
1101
- });
1102
- if (!this.isGenerating) {
1103
- msg.cachedReasoningBox = thinkLines;
1104
- msg.cachedWidth = innerChatW;
1105
- }
1128
+ chatContent.push(` ${theme.yellow("🧠 Raciocínio:")}`);
1106
1129
  }
1107
1130
 
1108
- for (const line of thinkLines) {
1109
- chatContent.push(` ${line}`);
1131
+ const rLines = formatReasoning(msg.reasoning, innerChatW - 8);
1132
+ for (const r of rLines) {
1133
+ chatContent.push(` ${r}`);
1110
1134
  }
1111
1135
  chatContent.push("");
1112
1136
  }
@@ -1131,11 +1155,18 @@ export class ChatView implements TuiView {
1131
1155
  chatContent.push(` ${theme.yellow(`${spinner} Pensando...`)}`);
1132
1156
  }
1133
1157
 
1134
- 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("");
1135
1165
  chatContent.push(
1136
- ` ${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}`,
1137
1167
  );
1138
1168
  }
1169
+ chatContent.push("");
1139
1170
  }
1140
1171
  }
1141
1172
 
@@ -1237,13 +1268,18 @@ export class ChatView implements TuiView {
1237
1268
  ? `${spinner} Gerando... (Esc para cancelar)`
1238
1269
  : actionLabel;
1239
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
+
1240
1276
  const inputBox = drawBox({
1241
1277
  title: inputTitle,
1242
1278
  width,
1243
1279
  height: 3,
1244
1280
  borderColor: this.isGenerating ? theme.yellow : theme.borderActive,
1245
1281
  titleColor: this.isGenerating ? theme.yellow : theme.cyan,
1246
- footer: this.statusNote ? stripAnsi(this.statusNote) : undefined,
1282
+ footer: inputFooter,
1247
1283
  content: inputContent,
1248
1284
  });
1249
1285
  totalLines.push(...inputBox);
@@ -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, truncate } from "../theme.ts";
7
+ import { theme, glyphs, drawBox, pad, truncate, setClipboardText } from "../theme.ts";
8
8
  import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
10
  import { getRuntimeChatMode, cycleNextChatMode } from "../../core/config.ts";
@@ -61,14 +61,21 @@ export class StatusView implements TuiView {
61
61
  private actionMessage = "";
62
62
  private actionMessageTimeout: NodeJS.Timeout | null = null;
63
63
  private hoveredActionRow: number | null = null;
64
+ private isBaseUrlHovered = false;
65
+ private copiedRecently = false;
66
+ private copiedTimeout: NodeJS.Timeout | null = null;
67
+ private lastBaseUrl = "http://127.0.0.1:7936/v1";
68
+ private lastBaseUrlRow = 6;
69
+ private lastModoApiRow = 7;
64
70
  private lastLeftW = 38;
65
71
  private lastActionRecarregarRow = 20;
66
72
  private lastActionZerarRow = 21;
67
73
  private lastActionModoRow = 22;
74
+ private lastActionCopiarRow = 23;
75
+
68
76
  constructor() {
69
77
  this.refresh();
70
78
  }
71
-
72
79
  public async refresh(): Promise<void> {
73
80
  try {
74
81
  if (process.stdout.isTTY && !process.env.NODE_TEST_CONTEXT) {
@@ -90,6 +97,7 @@ export class StatusView implements TuiView {
90
97
  { key: "r", label: "Recarregar" },
91
98
  { key: "z", label: "Zerar Cooldowns" },
92
99
  { key: "m", label: "Alternar Modo" },
100
+ { key: "c", label: "Copiar URL" },
93
101
  ];
94
102
  }
95
103
 
@@ -102,25 +110,34 @@ export class StatusView implements TuiView {
102
110
  }
103
111
 
104
112
  public async handleKey(key: KeyEvent): Promise<boolean | void> {
105
- // Mouse hover over quick actions
113
+ // Mouse hover over quick actions or Base URL
106
114
  if (key.name === "hover" && key.mouse) {
107
115
  const { row, col } = key.mouse;
108
116
  const leftW = this.lastLeftW || 38;
109
- if (
110
- col >= 2 &&
111
- col <= leftW - 1 &&
117
+ const isOverLeft = col >= 2 && col <= leftW - 1;
118
+ const isOverBaseUrl = isOverLeft && row === this.lastBaseUrlRow;
119
+ const isOverAction =
120
+ isOverLeft &&
112
121
  (row === this.lastActionRecarregarRow ||
113
122
  row === this.lastActionZerarRow ||
114
- row === this.lastActionModoRow)
115
- ) {
123
+ row === this.lastActionModoRow ||
124
+ row === this.lastActionCopiarRow);
125
+
126
+ let changed = false;
127
+ if (isOverBaseUrl !== this.isBaseUrlHovered) {
128
+ this.isBaseUrlHovered = isOverBaseUrl;
129
+ changed = true;
130
+ }
131
+ if (isOverAction) {
116
132
  if (this.hoveredActionRow !== row) {
117
133
  this.hoveredActionRow = row;
118
- return true;
134
+ changed = true;
119
135
  }
120
136
  } else if (this.hoveredActionRow !== null) {
121
137
  this.hoveredActionRow = null;
122
- return true;
138
+ changed = true;
123
139
  }
140
+ if (changed) return true;
124
141
  }
125
142
 
126
143
  // Mouse click interactions
@@ -128,6 +145,17 @@ export class StatusView implements TuiView {
128
145
  const { row, col } = key.mouse;
129
146
  const leftW = this.lastLeftW || 38;
130
147
  if (col >= 2 && col <= leftW - 1) {
148
+ if (row === this.lastBaseUrlRow || row === this.lastActionCopiarRow) {
149
+ setClipboardText(this.lastBaseUrl);
150
+ this.copiedRecently = true;
151
+ if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
152
+ this.copiedTimeout = setTimeout(() => {
153
+ this.copiedRecently = false;
154
+ this.copiedTimeout = null;
155
+ }, 2500);
156
+ this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
157
+ return true;
158
+ }
131
159
  if (row === this.lastActionRecarregarRow) {
132
160
  await this.refresh();
133
161
  this.setMessage(theme.green("✓ Status atualizado"));
@@ -139,7 +167,7 @@ export class StatusView implements TuiView {
139
167
  this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
140
168
  return true;
141
169
  }
142
- if (row === this.lastActionModoRow) {
170
+ if (row === this.lastActionModoRow || row === this.lastModoApiRow) {
143
171
  const nextMode = cycleNextChatMode();
144
172
  saveTuiSettings({ chat: { mode: nextMode } });
145
173
  await this.refresh();
@@ -168,8 +196,19 @@ export class StatusView implements TuiView {
168
196
  this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
169
197
  return true;
170
198
  }
171
- }
172
199
 
200
+ if ((key.name === "c" || key.name === "C") && !key.ctrl && !key.meta) {
201
+ setClipboardText(this.lastBaseUrl);
202
+ this.copiedRecently = true;
203
+ if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
204
+ this.copiedTimeout = setTimeout(() => {
205
+ this.copiedRecently = false;
206
+ this.copiedTimeout = null;
207
+ }, 2500);
208
+ this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
209
+ return true;
210
+ }
211
+ }
173
212
  public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
174
213
  const data = snapshot || this.statusData;
175
214
  const isOnline = data?.online ?? false;
@@ -197,7 +236,9 @@ export class StatusView implements TuiView {
197
236
  const uptimeSecs = data?.uptimeSeconds || Math.floor(process.uptime());
198
237
  const uptimeStr = formatUptime(uptimeSecs);
199
238
  const baseUrl = `http://${data?.host || "127.0.0.1"}:${data?.port || 7936}/v1`;
200
-
239
+ this.lastBaseUrl = baseUrl;
240
+ this.lastBaseUrlRow = 6;
241
+ this.lastModoApiRow = 7;
201
242
  const m = data?.metrics;
202
243
  const reqsTotal = m?.requestsTotal ?? 0;
203
244
  const reqsErrors = m?.requestsErrors ?? 0;
@@ -230,10 +271,18 @@ export class StatusView implements TuiView {
230
271
  if (data?.waitingStreams && data.waitingStreams > 0) {
231
272
  connsStr += theme.peach(` (${data.waitingStreams} na fila)`);
232
273
  }
274
+ let urlDisplay: string;
275
+ if (this.copiedRecently) {
276
+ urlDisplay = theme.bold(theme.green(baseUrl));
277
+ } else if (this.isBaseUrlHovered) {
278
+ urlDisplay = theme.bold(theme.underline(theme.cyan(baseUrl)));
279
+ } else {
280
+ urlDisplay = theme.cyan(baseUrl);
281
+ }
233
282
 
234
283
  const leftContent: string[] = [
235
284
  ` ${theme.bold(lbl("Status:"))} ${onlineBadge}`,
236
- ` ${theme.bold(lbl("Base URL:"))} ${theme.cyan(baseUrl)}`,
285
+ ` ${theme.bold(lbl("Base URL:"))} ${urlDisplay}`,
237
286
  ` ${theme.bold(lbl("Modo API:"))} ${
238
287
  getRuntimeChatMode() === "thread"
239
288
  ? theme.cyan("[thread]")
@@ -261,15 +310,19 @@ export class StatusView implements TuiView {
261
310
 
262
311
  const recarregarIdx = leftContent.length;
263
312
  const zerarIdx = leftContent.length + 1;
313
+ const modoIdx = leftContent.length + 2;
314
+ const copiarIdx = leftContent.length + 3;
315
+
264
316
  this.lastActionRecarregarRow = 5 + recarregarIdx;
265
317
  this.lastActionZerarRow = 5 + zerarIdx;
266
- const modoIdx = leftContent.length + 2;
267
318
  this.lastActionModoRow = 5 + modoIdx;
319
+ this.lastActionCopiarRow = 5 + copiarIdx;
268
320
 
269
321
  leftContent.push(
270
322
  ` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
271
323
  ` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
272
324
  ` ${this.hoveredActionRow === this.lastActionModoRow ? theme.bgHover(` ${theme.lavender("[ M ] Alternar Modo")} `) : ` ${theme.lavender("[ M ]")} Alternar Modo`}`,
325
+ ` ${this.hoveredActionRow === this.lastActionCopiarRow ? theme.bgHover(` ${theme.green("[ C ] Copiar URL")} `) : ` ${theme.green("[ C ]")} Copiar URL`}`,
273
326
  );
274
327
 
275
328
  const boxHeight = Math.max(contentH, leftContent.length + 2);