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/README.md +2 -2
- package/bin/qwenproxy.js +41 -13
- package/package.json +1 -1
- package/src/api/server.ts +4 -1
- package/src/clean-cache.ts +17 -11
- package/src/core/config.ts +2 -0
- package/src/core/model-alias.ts +47 -7
- package/src/login.ts +5 -0
- package/src/routes/chat/validation.ts +1 -1
- package/src/services/fingerprint.ts +45 -5
- package/src/services/playwright.ts +120 -8
- package/src/services/qwen-chat-pool.ts +4 -4
- package/src/services/qwen-headers.ts +28 -6
- package/src/sync/omp.ts +27 -21
- package/src/sync/opencode.ts +34 -30
- package/src/tui/app.ts +1 -1
- package/src/tui/proxy-client.ts +50 -49
- package/src/tui/screen.ts +47 -7
- package/src/tui/server-manager.ts +19 -11
- package/src/tui/theme.ts +41 -23
- package/src/tui/views/accounts-view.ts +5 -3
- package/src/tui/views/chat-view.ts +1 -0
- package/src/tui/views/status-view.ts +4 -4
- package/src/tui/views/storage-view.ts +11 -7
- package/src/update-cli.ts +21 -6
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(
|
|
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
|
-
|
|
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}`;
|
package/src/sync/opencode.ts
CHANGED
|
@@ -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(
|
|
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 :
|
|
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 = "
|
|
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");
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -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
|
|
282
|
+
let liveModelsPromise: Promise<string[]> | null = null;
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
295
|
-
isFetchingLiveModels = true;
|
|
311
|
+
liveModelsPromise = (async () => {
|
|
296
312
|
const controller = new AbortController();
|
|
297
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
192
|
-
const code =
|
|
193
|
-
//
|
|
194
|
-
if (
|
|
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 ===
|
|
211
|
-
code ===
|
|
212
|
-
|
|
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
|
|
248
|
-
for (const
|
|
249
|
-
const
|
|
250
|
-
if (currentW +
|
|
251
|
-
currentW +=
|
|
252
|
-
|
|
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
|
|
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
|
|
383
|
-
//
|
|
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)
|
|
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(
|
|
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,
|
|
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(
|
|
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}
|
|
182
|
+
rightContent.push(` ${num} ${name} ${status}`);
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|