qwenproxy-cli 1.0.4 → 1.0.6

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/sync/omp.ts CHANGED
@@ -3,7 +3,30 @@ import path from "node:path";
3
3
  import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
4
  import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
5
 
6
- function buildOmpProviderYaml(baseUrl: string, apiKey: string): string {
6
+ function buildOmpProviderYaml(
7
+ baseUrl: string,
8
+ apiKey: string,
9
+ primaryModel: string = "qwen3.8-max",
10
+ ): string {
11
+ const modelList = [primaryModel];
12
+ if (primaryModel !== "qwen3.7-plus") {
13
+ modelList.push("qwen3.7-plus");
14
+ }
15
+
16
+ const formattedModels = modelList
17
+ .map(
18
+ (m) => ` - id: ${m}
19
+ name: ${m === "qwen3.8-max" ? "Qwen3.8-Max" : m === "qwen3.7-plus" ? "Qwen3.7-Plus" : m}
20
+ input: [text, image]
21
+ contextWindow: 1000000
22
+ maxTokens: 131072
23
+ reasoning: true
24
+ thinking:
25
+ mode: effort
26
+ efforts: [low, medium, high]`,
27
+ )
28
+ .join("\n");
29
+
7
30
  return ` qwenproxy:
8
31
  baseUrl: ${baseUrl}
9
32
  api: openai-completions
@@ -13,29 +36,12 @@ function buildOmpProviderYaml(baseUrl: string, apiKey: string): string {
13
36
  supportsReasoningEffort: true
14
37
  maxTokensField: max_completion_tokens
15
38
  models:
16
- - id: qwen3.8-max
17
- name: Qwen3.8-Max
18
- input: [text, image]
19
- contextWindow: 1000000
20
- maxTokens: 131072
21
- reasoning: true
22
- thinking:
23
- mode: effort
24
- efforts: [low, medium, high]
25
- - id: qwen3.7-plus
26
- name: Qwen3.7-Plus
27
- input: [text, image]
28
- contextWindow: 1000000
29
- maxTokens: 131072
30
- reasoning: true
31
- thinking:
32
- mode: effort
33
- efforts: [low, medium, high]
39
+ ${formattedModels}
34
40
  `;
35
41
  }
36
42
 
37
43
  export function syncOmp(options: SyncOptions): ClientSyncResult {
38
- const { filePath, apiKey, baseUrl } = options;
44
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
39
45
  try {
40
46
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
47
 
@@ -47,7 +53,7 @@ export function syncOmp(options: SyncOptions): ClientSyncResult {
47
53
  content = fs.readFileSync(filePath, "utf-8");
48
54
  }
49
55
 
50
- const providerBlock = buildOmpProviderYaml(baseUrl, apiKey);
56
+ const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model);
51
57
 
52
58
  if (!content.trim()) {
53
59
  content = `providers:\n${providerBlock}`;
@@ -3,7 +3,36 @@ import path from "node:path";
3
3
  import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
4
  import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
5
 
6
- function buildOpenCodeProviderObject(baseUrl: string, apiKey: string): Record<string, any> {
6
+ function buildOpenCodeProviderObject(
7
+ baseUrl: string,
8
+ apiKey: string,
9
+ primaryModel: string = "qwen3.8-max",
10
+ ): Record<string, any> {
11
+ const modelsObj: Record<string, any> = {};
12
+ const modelList = [primaryModel];
13
+ if (primaryModel !== "qwen3.7-plus") {
14
+ modelList.push("qwen3.7-plus");
15
+ }
16
+
17
+ for (const m of modelList) {
18
+ modelsObj[m] = {
19
+ name:
20
+ m === "qwen3.8-max"
21
+ ? "Qwen 3.8 Max"
22
+ : m === "qwen3.7-plus"
23
+ ? "Qwen 3.7 Plus"
24
+ : m,
25
+ limit: { context: 1048576, output: 65536 },
26
+ modalities: { input: ["text", "image"], output: ["text"] },
27
+ reasoning: true,
28
+ variants: {
29
+ low: { effort: "low" },
30
+ medium: { effort: "medium" },
31
+ high: { effort: "high" },
32
+ },
33
+ };
34
+ }
35
+
7
36
  return {
8
37
  npm: "@ai-sdk/openai-compatible",
9
38
  name: "QwenProxy",
@@ -11,30 +40,7 @@ function buildOpenCodeProviderObject(baseUrl: string, apiKey: string): Record<st
11
40
  baseURL: baseUrl,
12
41
  apiKey: apiKey,
13
42
  },
14
- models: {
15
- "qwen3.8-max": {
16
- name: "Qwen 3.8 Max",
17
- limit: { context: 1048576, output: 65536 },
18
- modalities: { input: ["text", "image"], output: ["text"] },
19
- reasoning: true,
20
- variants: {
21
- low: { effort: "low" },
22
- medium: { effort: "medium" },
23
- high: { effort: "high" },
24
- },
25
- },
26
- "qwen3.7-plus": {
27
- name: "Qwen 3.7 Plus",
28
- limit: { context: 1048576, output: 65536 },
29
- modalities: { input: ["text", "image"], output: ["text"] },
30
- reasoning: true,
31
- variants: {
32
- low: { effort: "low" },
33
- medium: { effort: "medium" },
34
- high: { effort: "high" },
35
- },
36
- },
37
- },
43
+ models: modelsObj,
38
44
  };
39
45
  }
40
46
  function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
@@ -121,10 +127,8 @@ function findKeyObjectSpan(content: string, key: string): { start: number; end:
121
127
  }
122
128
 
123
129
  export function syncOpenCode(options: SyncOptions): ClientSyncResult {
124
- const { filePath, apiKey, baseUrl } = options;
130
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
125
131
  try {
126
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
127
-
128
132
  let backupPath: string | undefined;
129
133
  let content = "";
130
134
 
@@ -133,10 +137,10 @@ export function syncOpenCode(options: SyncOptions): ClientSyncResult {
133
137
  content = fs.readFileSync(filePath, "utf-8");
134
138
  }
135
139
 
136
- const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey);
140
+ const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model);
137
141
  const providerJson = JSON.stringify(providerObj, null, 6)
138
142
  .split("\n")
139
- .map((line, idx) => (idx === 0 ? line : " " + line))
143
+ .map((line, idx) => (idx === 0 ? line : ` ${line}`))
140
144
  .join("\n");
141
145
 
142
146
  const qwenEntry = ` "qwenproxy": ${providerJson}`;
package/src/tui/app.ts CHANGED
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import path from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
- let cachedAppVersion = "v1.0.4";
14
+ let cachedAppVersion = "";
15
15
  try {
16
16
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
17
17
  const pkgPath = path.resolve(currentDir, "../../package.json");
@@ -279,66 +279,67 @@ export async function streamChatCompletions(
279
279
  * Fetches all live models dynamically from the running proxy /v1/models catalog.
280
280
  */
281
281
  let cachedLiveModels: string[] | null = null;
282
- let isFetchingLiveModels = false;
282
+ let liveModelsPromise: Promise<string[]> | null = null;
283
283
 
284
- export async function fetchLiveModels(): Promise<string[]> {
285
- if (cachedLiveModels && cachedLiveModels.length > 0) {
284
+ const DEFAULT_FALLBACK_MODELS = [
285
+ "qwen3.8-max",
286
+ "qwen3.7-plus",
287
+ "qwen3.7-max",
288
+ "z-image-turbo",
289
+ "qwen-image-3.0-pro",
290
+ "qwen-image-3.0",
291
+ "wan2.7-image-pro",
292
+ "wan2.7-image",
293
+ "wan3.0-video",
294
+ "wan2.7-t2v",
295
+ ];
296
+
297
+ export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
298
+ if (!forceRefresh && cachedLiveModels && cachedLiveModels.length > 0) {
286
299
  return cachedLiveModels;
287
300
  }
288
301
 
302
+ if (liveModelsPromise) {
303
+ return liveModelsPromise;
304
+ }
305
+
289
306
  const port = config.server?.port || 7936;
290
307
  const configuredHost = config.server?.host;
291
308
  const host = configuredHost && configuredHost !== "0.0.0.0" ? configuredHost : "127.0.0.1";
292
309
  const apiKey = config.apiKey || "sk-qwenproxy-local";
293
310
 
294
- if (!isFetchingLiveModels) {
295
- isFetchingLiveModels = true;
311
+ liveModelsPromise = (async () => {
296
312
  const controller = new AbortController();
297
- const timeout = setTimeout(() => controller.abort(), 2500);
298
- fetch(`http://${host}:${port}/v1/models`, {
299
- headers: { Authorization: `Bearer ${apiKey}` },
300
- signal: controller.signal,
301
- })
302
- .then(async (resp) => {
303
- clearTimeout(timeout);
304
- if (resp.ok) {
305
- const json = (await resp.json()) as any;
306
- if (Array.isArray(json?.data)) {
307
- const models = json.data
308
- .map((m: any) => m.id)
309
- .filter((id: any): id is string => typeof id === "string" && id.trim().length > 0)
310
- .filter(
311
- (id: string) =>
312
- !id.endsWith("-fast") &&
313
- !id.endsWith("-thinking") &&
314
- !id.endsWith("-no-thinking"),
315
- );
316
- if (models.length > 0) {
317
- cachedLiveModels = Array.from(new Set(models));
318
- }
313
+ const timeout = setTimeout(() => controller.abort(), 3000);
314
+ try {
315
+ const resp = await fetch(`http://${host}:${port}/v1/models`, {
316
+ headers: { Authorization: `Bearer ${apiKey}` },
317
+ signal: controller.signal,
318
+ });
319
+ if (resp.ok) {
320
+ const json = (await resp.json()) as any;
321
+ if (Array.isArray(json?.data)) {
322
+ const models = json.data
323
+ .map((m: any) => m.id)
324
+ .filter((id: any): id is string => typeof id === "string" && id.trim().length > 0)
325
+ .filter(
326
+ (id: string) =>
327
+ !id.endsWith("-fast") &&
328
+ !id.endsWith("-thinking") &&
329
+ !id.endsWith("-no-thinking"),
330
+ );
331
+ if (models.length > 0) {
332
+ cachedLiveModels = Array.from(new Set(models));
333
+ return cachedLiveModels;
319
334
  }
320
335
  }
321
- })
322
- .catch(() => {
323
- clearTimeout(timeout);
324
- })
325
- .finally(() => {
326
- isFetchingLiveModels = false;
327
- });
328
- }
336
+ }
337
+ } catch {} finally {
338
+ clearTimeout(timeout);
339
+ liveModelsPromise = null;
340
+ }
341
+ return cachedLiveModels || DEFAULT_FALLBACK_MODELS;
342
+ })();
329
343
 
330
- return (
331
- cachedLiveModels || [
332
- "qwen3.8-max",
333
- "qwen3.7-plus",
334
- "qwen3.7-max",
335
- "z-image-turbo",
336
- "qwen-image-3.0-pro",
337
- "qwen-image-3.0",
338
- "wan2.7-image-pro",
339
- "wan2.7-image",
340
- "wan3.0-video",
341
- "wan2.7-t2v",
342
- ]
343
- );
344
+ return liveModelsPromise;
344
345
  }
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
  }
@@ -425,8 +425,10 @@ export class AccountsView implements TuiView {
425
425
  onConfirm: async () => {
426
426
  removeAccount(selected.id);
427
427
  try {
428
- const { closePlaywrightForAccount } = await import("../../services/playwright.ts");
428
+ const { closePlaywrightForAccount, removePlaywrightProfile } = await import("../../services/playwright.ts");
429
+ const { getAccountProfilePath } = await import("../../core/paths.ts");
429
430
  await closePlaywrightForAccount(selected.id);
431
+ removePlaywrightProfile(getAccountProfilePath(selected.id));
430
432
  } catch {}
431
433
  await this.refresh();
432
434
  this.setStatusMessage(theme.green(`✓ Conta ${selected.emailOrName} removida com sucesso`));
@@ -633,12 +635,12 @@ export class AccountsView implements TuiView {
633
635
  const isHovered = idx === this.hoveredAccountIndex;
634
636
  const pointer = isFocused ? theme.cyan(`${glyphs.pointer} `) : " ";
635
637
  const num = pad(String(idx + 1) + ".", 4);
636
- const name = pad(acc.emailOrName, 22);
638
+ const name = pad(truncate(acc.emailOrName, 20), 22);
637
639
 
638
640
  let status = theme.green(`${glyphs.bullet} Pronto `);
639
641
  if (acc.onCooldown) {
640
642
  const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
641
- status = theme.yellow(`⚠ ${mins}m cd `);
643
+ status = theme.yellow(`⚠️ ${mins}m cd `);
642
644
  } else if (!acc.headersReady) {
643
645
  status = acc.isInitialized
644
646
  ? theme.yellow(`◐ Aquecendo...`)
@@ -341,6 +341,7 @@ export class ChatView implements TuiView {
341
341
  (key.ctrl && key.name === "o") ||
342
342
  (key.meta && key.name === "m")
343
343
  ) {
344
+ void this.refreshModels();
344
345
  this.isModelModalOpen = true;
345
346
  this.modalSelectedIndex = this.selectedModelIndex;
346
347
  this.onNeedsRender?.();
@@ -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