qwenproxy-cli 1.0.26 → 1.0.28

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.
@@ -13,6 +13,7 @@ import {
13
13
  import { isPlaywrightInitialized } from "../services/playwright.ts";
14
14
  import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
15
15
  import { getRssUsageSnapshot } from "../core/memory-usage.ts";
16
+ import { metrics } from "../core/metrics.ts";
16
17
  import type { ProxyStatusSnapshot } from "./types.ts";
17
18
 
18
19
  export function maskAccountIdentifier(idOrEmail: string): string {
@@ -55,6 +56,7 @@ let lastOnlineState = false;
55
56
  let lastOverallStatus = "offline";
56
57
  let lastServerReadyAccounts: Set<string> | null = null;
57
58
  let lastServerActiveAccounts: Set<string> | null = null;
59
+ let lastMetricsData: any = null;
58
60
  export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
59
61
  const port = config.server?.port || 7936;
60
62
  const configuredHost = config.server?.host;
@@ -79,6 +81,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
79
81
  if (Array.isArray(data.activeAccounts)) {
80
82
  lastServerActiveAccounts = new Set(data.activeAccounts);
81
83
  }
84
+ if (data.metrics) {
85
+ lastMetricsData = data.metrics;
86
+ }
82
87
  } else {
83
88
  lastOnlineState = false;
84
89
  lastServerReadyAccounts = null;
@@ -106,8 +111,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
106
111
 
107
112
  cachedAccounts = rawAccounts.map((acc) => {
108
113
  const cooldownInfo = getAccountCooldownInfo(acc.id);
109
- const onCooldown = Boolean(cooldownInfo?.onCooldown);
110
- const remainingCooldownMs = cooldownInfo?.remainingMs || 0;
114
+ const onCooldown = Boolean(cooldownInfo?.onCooldown || (acc.cooldown_until && acc.cooldown_until > now));
115
+ const remainingCooldownMs = cooldownInfo?.remainingMs || (acc.cooldown_until && acc.cooldown_until > now ? acc.cooldown_until - now : 0);
116
+ const cooldownReason = cooldownInfo?.reason || acc.cooldown_reason || (onCooldown ? "RateLimited" : null);
111
117
  const headersReady = lastServerReadyAccounts !== null
112
118
  ? lastServerReadyAccounts.has(acc.id)
113
119
  : isAccountHeadersReady(acc.id);
@@ -121,26 +127,37 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
121
127
  cooldownUntil: acc.cooldown_until || null,
122
128
  onCooldown,
123
129
  remainingCooldownMs,
130
+ cooldownReason,
124
131
  headersReady,
125
132
  isInitialized,
126
133
  };
127
134
  });
128
135
  }
129
- const accounts = cachedAccounts;
130
136
  const online = lastOnlineState;
131
137
  const overallStatus = lastOverallStatus;
132
138
 
133
139
  // Concurrency stats
134
140
  let activeStreams = 0;
135
141
  let waitingStreams = 0;
142
+ const concurrencyMap = new Map<string, { active: number; waiting: number; limit: number }>();
136
143
  try {
137
144
  const snapshot = getAccountConcurrencySnapshot();
138
145
  for (const item of snapshot) {
139
146
  activeStreams += item.active;
140
147
  waitingStreams += item.waiting;
148
+ concurrencyMap.set(item.accountId, item);
141
149
  }
142
150
  } catch {}
143
151
 
152
+ // Attach concurrency to accounts
153
+ const accounts = cachedAccounts.map((acc) => {
154
+ const concurrency = concurrencyMap.get(acc.id);
155
+ return {
156
+ ...acc,
157
+ activeStreams: concurrency?.active ?? 0,
158
+ streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
159
+ };
160
+ });
144
161
  // RAM usage
145
162
  let rssMb = 0;
146
163
  let systemMemoryPct = 0;
@@ -150,6 +167,28 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
150
167
  systemMemoryPct = Math.round(rssSnap.usagePercent * 10) / 10;
151
168
  } catch {}
152
169
 
170
+ const totalReqs = lastMetricsData?.requestsTotal ?? Number(metrics.get("requests.total")?.value ?? 0);
171
+ const totalErrs = lastMetricsData?.requestsErrors ?? Number(metrics.get("requests.errors")?.value ?? 0);
172
+ const successRate = totalReqs > 0 ? Number((((totalReqs - totalErrs) / totalReqs) * 100).toFixed(1)) : 100;
173
+ const latencyAvgMs = lastMetricsData?.latencyAvgMs ?? (() => {
174
+ const hist = metrics.get("latency.request")?.value;
175
+ if (hist && typeof hist === "object" && (hist as any).count > 0) {
176
+ return Math.round((hist as any).sum / (hist as any).count);
177
+ }
178
+ return 0;
179
+ })();
180
+ const deltasCount = lastMetricsData?.deltasCount ?? Number(metrics.get("requests.delta")?.value ?? 0);
181
+ const fullReplaysCount = lastMetricsData?.fullReplaysCount ?? Number(metrics.get("requests.full")?.value ?? 0);
182
+ const totalModes = deltasCount + fullReplaysCount;
183
+ const deltaRatio = totalModes > 0 ? Number(((deltasCount / totalModes) * 100).toFixed(1)) : (deltasCount > 0 ? 100 : 0);
184
+ const toolCallsCount = lastMetricsData?.toolCallsCount ?? Number(metrics.get("toolcalls.total")?.value ?? 0);
185
+ const toolCallsRecovered = lastMetricsData?.toolCallsRecovered ?? Number(metrics.get("toolcalls.recovered")?.value ?? 0);
186
+ const captchasDetected = lastMetricsData?.captchasDetected ?? Number(metrics.get("captcha.challenges.detected")?.value ?? 0);
187
+ const captchasSolved = lastMetricsData?.captchasSolved ?? Number(metrics.get("captcha.solves.succeeded")?.value ?? 0);
188
+ const chatsCleaned = lastMetricsData?.chatsCleaned ?? Number(metrics.get("chats.cleaned")?.value ?? 0);
189
+ const cacheHitRatio = lastMetricsData?.cache?.hitRatio;
190
+ const cacheBytesSaved = lastMetricsData?.cache?.bytesSaved;
191
+
153
192
  return {
154
193
  online,
155
194
  port,
@@ -160,10 +199,25 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
160
199
  systemMemoryPct,
161
200
  activeStreams,
162
201
  waitingStreams,
202
+ metrics: {
203
+ requestsTotal: totalReqs,
204
+ requestsErrors: totalErrs,
205
+ successRate,
206
+ latencyAvgMs,
207
+ deltasCount,
208
+ fullReplaysCount,
209
+ deltaRatio,
210
+ toolCallsCount,
211
+ toolCallsRecovered,
212
+ captchasDetected,
213
+ captchasSolved,
214
+ chatsCleaned,
215
+ cacheHitRatio,
216
+ cacheBytesSaved,
217
+ },
163
218
  accounts,
164
219
  };
165
220
  }
166
-
167
221
  export function resetAllCooldowns(): number {
168
222
  return clearAllAccountCooldowns();
169
223
  }
@@ -31,6 +31,7 @@ export class ServerManager {
31
31
  private intercepted = false;
32
32
  private isTuiRendering = false;
33
33
  private startPromise: Promise<void> | null = null;
34
+ private remoteLogAbort: AbortController | null = null;
34
35
 
35
36
  public static getInstance(): ServerManager {
36
37
  if (!ServerManager.instance) {
@@ -112,6 +113,11 @@ export class ServerManager {
112
113
  ) {
113
114
  continue;
114
115
  }
116
+ // Clean redundant leading level tags (e.g. "WARN [Qwen]" -> "[Qwen]")
117
+ // and normalize multi-space gaps after emojis
118
+ line = line
119
+ .replace(/^(?:\[?(?:INFO|WARN|WARNING|ERROR|ERR|DEBUG)\]?\s+)+/i, "")
120
+ .replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
115
121
  if (!line) continue;
116
122
 
117
123
  // Prevent identical consecutive duplicate logs in the same second
@@ -234,6 +240,7 @@ export class ServerManager {
234
240
  "INFO",
235
241
  `✨ [Server] Conectado à instância em execução na porta ${port}`,
236
242
  );
243
+ this.startRemoteLogStream(cleanHost, port);
237
244
  return;
238
245
  }
239
246
  } catch {}
@@ -269,10 +276,53 @@ export class ServerManager {
269
276
  }
270
277
 
271
278
  public async stop(): Promise<void> {
279
+ if (this.remoteLogAbort) {
280
+ this.remoteLogAbort.abort();
281
+ this.remoteLogAbort = null;
282
+ }
272
283
  this.restoreLogs();
273
284
  try {
274
- await stopServer();
275
285
  this.state = "offline";
276
286
  } catch {}
277
287
  }
288
+
289
+ public startRemoteLogStream(host: string, port: number): void {
290
+ if (this.remoteLogAbort) {
291
+ this.remoteLogAbort.abort();
292
+ }
293
+ const abort = new AbortController();
294
+ this.remoteLogAbort = abort;
295
+
296
+ (async () => {
297
+ try {
298
+ const resp = await fetch(`http://${host}:${port}/logs/live`, {
299
+ signal: abort.signal,
300
+ headers: { Accept: "text/event-stream" },
301
+ });
302
+ if (!resp.ok || !resp.body) return;
303
+ const reader = resp.body.getReader();
304
+ const decoder = new TextDecoder();
305
+ let buf = "";
306
+
307
+ while (!abort.signal.aborted) {
308
+ const { done, value } = await reader.read();
309
+ if (done) break;
310
+ buf += decoder.decode(value, { stream: true });
311
+ const lines = buf.split("\n");
312
+ buf = lines.pop() ?? "";
313
+
314
+ for (const line of lines) {
315
+ if (line.startsWith("data: ")) {
316
+ try {
317
+ const data = JSON.parse(line.slice(6));
318
+ if (data && data.message) {
319
+ this.appendLog(data.level || "INFO", data.message);
320
+ }
321
+ } catch {}
322
+ }
323
+ }
324
+ }
325
+ } catch {}
326
+ })();
327
+ }
278
328
  }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * QwenProxy TUI - User Preferences & Settings Persistence
3
+ * Automatically saves and restores chosen model, reasoning effort, last active tab, and log filters.
4
+ */
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { getTuiSettingsPath } from "../core/paths.ts";
9
+
10
+ export interface TuiSettings {
11
+ lastTab?: number;
12
+ chat?: {
13
+ model?: string;
14
+ effort?: "high" | "medium" | "low";
15
+ };
16
+ logs?: {
17
+ filter?: "all" | "warn" | "error";
18
+ };
19
+ }
20
+
21
+ const defaultSettings: TuiSettings = {
22
+ lastTab: 1,
23
+ chat: {
24
+ model: "qwen3.8-max",
25
+ effort: "high",
26
+ },
27
+ logs: {
28
+ filter: "all",
29
+ },
30
+ };
31
+
32
+ let cachedSettings: TuiSettings | null = null;
33
+
34
+ export function resetTuiSettingsCacheForTests(): void {
35
+ cachedSettings = null;
36
+ }
37
+
38
+ export function loadTuiSettings(): TuiSettings {
39
+ if (cachedSettings) {
40
+ return {
41
+ ...cachedSettings,
42
+ chat: { ...cachedSettings.chat },
43
+ logs: { ...cachedSettings.logs },
44
+ };
45
+ }
46
+
47
+ const filePath = getTuiSettingsPath();
48
+ try {
49
+ if (fs.existsSync(filePath)) {
50
+ const raw = fs.readFileSync(filePath, "utf-8");
51
+ const parsed = JSON.parse(raw);
52
+ const loaded: TuiSettings = {
53
+ ...defaultSettings,
54
+ ...parsed,
55
+ chat: { ...defaultSettings.chat, ...(parsed.chat || {}) },
56
+ logs: { ...defaultSettings.logs, ...(parsed.logs || {}) },
57
+ };
58
+ cachedSettings = loaded;
59
+ return {
60
+ ...loaded,
61
+ chat: { ...loaded.chat },
62
+ logs: { ...loaded.logs },
63
+ };
64
+ }
65
+ } catch {}
66
+
67
+ const finalSettings: TuiSettings = {
68
+ ...defaultSettings,
69
+ chat: { ...defaultSettings.chat },
70
+ logs: { ...defaultSettings.logs },
71
+ };
72
+ cachedSettings = finalSettings;
73
+ return {
74
+ ...finalSettings,
75
+ chat: { ...finalSettings.chat },
76
+ logs: { ...finalSettings.logs },
77
+ };
78
+ }
79
+
80
+ export function saveTuiSettings(updates: Partial<TuiSettings>): void {
81
+ try {
82
+ const current = loadTuiSettings();
83
+ const merged: TuiSettings = {
84
+ ...current,
85
+ ...updates,
86
+ chat: { ...current.chat, ...(updates.chat || {}) },
87
+ logs: { ...current.logs, ...(updates.logs || {}) },
88
+ };
89
+
90
+ cachedSettings = merged;
91
+
92
+ const filePath = getTuiSettingsPath();
93
+ const dir = path.dirname(filePath);
94
+ if (!fs.existsSync(dir)) {
95
+ fs.mkdirSync(dir, { recursive: true });
96
+ }
97
+
98
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf-8");
99
+ } catch {}
100
+ }
package/src/tui/theme.ts CHANGED
@@ -47,11 +47,14 @@ export const theme = {
47
47
 
48
48
  import { execSync, spawnSync } from "node:child_process";
49
49
 
50
+ let memoryClipboard = "";
51
+
50
52
  /**
51
53
  * Safely writes text to the system clipboard on Windows/macOS/Linux.
52
54
  * Also emits OSC 52 to copy inside terminal emulators supporting it.
53
55
  */
54
56
  export function setClipboardText(text: string): boolean {
57
+ memoryClipboard = text;
55
58
  try {
56
59
  // 1. Emit OSC 52 sequence for terminal emulators supporting it natively (only when interactive TTY)
57
60
  try {
@@ -64,8 +67,7 @@ export function setClipboardText(text: string): boolean {
64
67
  // 2. OS-level clipboard utility
65
68
  if (process.platform === "win32") {
66
69
  const p = spawnSync("clip.exe", {
67
- input: text,
68
- encoding: "utf-8",
70
+ input: Buffer.from(text, "utf16le"),
69
71
  windowsHide: true,
70
72
  });
71
73
  return p.status === 0;
@@ -90,17 +92,21 @@ export function setClipboardText(text: string): boolean {
90
92
  export function getClipboardText(): string {
91
93
  try {
92
94
  if (process.platform === "win32") {
93
- return execSync("powershell -NoProfile -Command Get-Clipboard", {
94
- timeout: 1000,
95
- windowsHide: true,
96
- stdio: ["pipe", "pipe", "ignore"],
97
- })
98
- .toString()
95
+ const res = execSync(
96
+ 'powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard"',
97
+ {
98
+ timeout: 2000,
99
+ windowsHide: true,
100
+ stdio: ["pipe", "pipe", "ignore"],
101
+ },
102
+ )
103
+ .toString("utf8")
99
104
  .replace(/\r?\n/g, "")
100
105
  .trim();
106
+ if (res) return res;
101
107
  }
102
108
  } catch {}
103
- return "";
109
+ return memoryClipboard;
104
110
  }
105
111
 
106
112
  export const glyphs = {
package/src/tui/types.ts CHANGED
@@ -21,6 +21,22 @@ export interface ProxyStatusSnapshot {
21
21
  systemMemoryPct?: number;
22
22
  activeStreams?: number;
23
23
  waitingStreams?: number;
24
+ metrics?: {
25
+ requestsTotal: number;
26
+ requestsErrors: number;
27
+ successRate: number;
28
+ latencyAvgMs: number;
29
+ deltasCount: number;
30
+ fullReplaysCount: number;
31
+ deltaRatio: number;
32
+ toolCallsCount: number;
33
+ toolCallsRecovered: number;
34
+ captchasDetected: number;
35
+ captchasSolved: number;
36
+ chatsCleaned: number;
37
+ cacheHitRatio?: number;
38
+ cacheBytesSaved?: number;
39
+ };
24
40
  accounts: Array<{
25
41
  id: string;
26
42
  emailOrName: string;
@@ -28,7 +44,10 @@ export interface ProxyStatusSnapshot {
28
44
  cooldownUntil: number | null;
29
45
  onCooldown: boolean;
30
46
  remainingCooldownMs: number;
47
+ cooldownReason?: string | null;
31
48
  headersReady: boolean;
32
49
  isInitialized?: boolean;
50
+ activeStreams?: number;
51
+ streamLimit?: number;
33
52
  }>;
34
53
  }
@@ -13,6 +13,33 @@ import {
13
13
  import { addAccount, removeAccount } from "../../core/accounts.ts";
14
14
  import { ServerManager } from "../server-manager.ts";
15
15
  import { config } from "../../core/config.ts";
16
+ export function formatCooldownReason(reason?: string | null, maxLen = 28): string {
17
+ if (!reason) return theme.yellow("Cooldown ativo");
18
+ if (
19
+ reason.startsWith("AuthFailed") ||
20
+ reason.startsWith("AuthPermanentFailure") ||
21
+ reason.includes("All login methods exhausted")
22
+ ) {
23
+ return theme.red(truncate("❌ Senha/Login inválido", maxLen));
24
+ }
25
+ if (reason === "AuthInitFailed") {
26
+ return theme.yellow(truncate("⚠️ Timeout Inicial (WAF/Headers)", maxLen));
27
+ }
28
+ if (reason === "RateLimited" || reason === "QuotaExceeded") {
29
+ return theme.yellow(truncate("⏳ Cota Excedida (Reset 00:00 UTC)", maxLen));
30
+ }
31
+ if (reason === "WafChallenge") {
32
+ return theme.peach(truncate("🛡️ Bloqueio WAF/Anti-Bot", maxLen));
33
+ }
34
+ if (reason.startsWith("StandbyValidationError")) {
35
+ return theme.red(truncate("❌ Falha Validação Standby", maxLen));
36
+ }
37
+ if (reason === "MediaGenFailed") {
38
+ return theme.yellow(truncate("⚠️ Falha Geração de Mídia", maxLen));
39
+ }
40
+ return theme.yellow(truncate(reason, maxLen));
41
+ }
42
+
16
43
  export class AccountsView implements TuiView {
17
44
  public readonly id = "accounts";
18
45
  public readonly title = "Contas";
@@ -662,8 +689,22 @@ export class AccountsView implements TuiView {
662
689
 
663
690
  let status = theme.green(`${glyphs.bullet} Pronto `);
664
691
  if (acc.onCooldown) {
665
- const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
666
- status = theme.yellow(`⚠️ ${mins}m cd `);
692
+ const reason = acc.cooldownReason || "";
693
+ if (
694
+ reason.startsWith("AuthFailed") ||
695
+ reason.startsWith("AuthPermanentFailure") ||
696
+ reason.includes("All login methods exhausted")
697
+ ) {
698
+ status = theme.red(`❌ Auth Fail `);
699
+ } else if (reason === "WafChallenge") {
700
+ status = theme.peach(`🛡️ WAF Block `);
701
+ } else if (reason === "AuthInitFailed") {
702
+ const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
703
+ status = theme.yellow(`⚠️ ${mins}m init `);
704
+ } else {
705
+ const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
706
+ status = theme.yellow(`⚠️ ${mins}m cd `);
707
+ }
667
708
  } else if (!acc.headersReady) {
668
709
  status = acc.isInitialized
669
710
  ? theme.yellow(`◐ Aquecendo...`)
@@ -724,7 +765,13 @@ export class AccountsView implements TuiView {
724
765
  ? theme.yellow(`◐ Aquecendo...`)
725
766
  : theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);
726
767
  rightContent.push(` ${theme.bold("Headers:")} ${hStatus}`);
727
- rightContent.push("");
768
+ if (selected.onCooldown && selected.cooldownReason) {
769
+ const maxReasonW = Math.max(16, rightW - 14);
770
+ const cdReason = formatCooldownReason(selected.cooldownReason, maxReasonW);
771
+ rightContent.push(` ${theme.bold("Motivo:")} ${cdReason}`);
772
+ } else {
773
+ rightContent.push("");
774
+ }
728
775
  rightContent.push(` ${theme.dim("─────────────────────────────────")}`);
729
776
  rightContent.push(` ${this.hoveredActionRow === 15 ? theme.bgHover(` ${theme.cyan("[ A ] Adicionar Conta")} `) : `${theme.cyan("[ A ]")} Adicionar Conta`}`);
730
777
  rightContent.push(` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ D ] Remover Conta")} `) : `${theme.red("[ D ]")} Remover Conta`}`);
@@ -8,6 +8,7 @@ import { theme, glyphs, drawBox, stringWidth, truncate, stripAnsi, pad, wrapCont
8
8
  import { streamChatCompletions, fetchLiveModels } from "../proxy-client.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
10
  import { formatMarkdown, formatReasoning } from "../markdown.ts";
11
+ import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
11
12
 
12
13
  interface ChatMessage {
13
14
  role: "user" | "assistant";
@@ -122,6 +123,21 @@ export class ChatView implements TuiView {
122
123
 
123
124
  constructor(onNeedsRender?: () => void) {
124
125
  this.onNeedsRender = onNeedsRender;
126
+ const saved = loadTuiSettings();
127
+ if (saved.chat?.model) {
128
+ const idx = this.availableModels.indexOf(saved.chat.model);
129
+ if (idx !== -1) {
130
+ this.selectedModelIndex = idx;
131
+ }
132
+ }
133
+ const savedEffort = saved.chat?.effort;
134
+ if (savedEffort && ["high", "medium", "low"].includes(savedEffort)) {
135
+ this.selectedEffort = savedEffort;
136
+ const effIdx = this.availableEfforts.findIndex((e) => e.id === savedEffort);
137
+ if (effIdx !== -1) {
138
+ this.effortSelectedIndex = effIdx;
139
+ }
140
+ }
125
141
  void this.refreshModels();
126
142
  }
127
143
  public onActivate(): void {
@@ -138,7 +154,13 @@ export class ChatView implements TuiView {
138
154
  if (live.length > 0) {
139
155
  const current = this.availableModels[this.selectedModelIndex];
140
156
  this.availableModels = live;
141
- const foundIdx = this.availableModels.indexOf(current);
157
+ let foundIdx = this.availableModels.indexOf(current);
158
+ if (foundIdx === -1) {
159
+ const saved = loadTuiSettings();
160
+ if (saved.chat?.model) {
161
+ foundIdx = this.availableModels.indexOf(saved.chat.model);
162
+ }
163
+ }
142
164
  this.selectedModelIndex = foundIdx !== -1 ? foundIdx : 0;
143
165
  this.onNeedsRender?.();
144
166
  }
@@ -169,7 +191,12 @@ export class ChatView implements TuiView {
169
191
  if (!chosen) return;
170
192
  this.selectedModelIndex = idx;
171
193
  this.isModelModalOpen = false;
172
-
194
+ saveTuiSettings({
195
+ chat: {
196
+ model: chosen,
197
+ effort: this.selectedEffort,
198
+ },
199
+ });
173
200
  const info = classifyModel(chosen);
174
201
  if (info.category === "Texto & Raciocínio") {
175
202
  this.isEffortModalOpen = true;
@@ -249,6 +276,12 @@ export class ChatView implements TuiView {
249
276
  this.selectedEffort = this.availableEfforts[row - 9].id;
250
277
  this.isEffortModalOpen = false;
251
278
  const currentM = this.availableModels[this.selectedModelIndex];
279
+ saveTuiSettings({
280
+ chat: {
281
+ model: currentM,
282
+ effort: this.selectedEffort,
283
+ },
284
+ });
252
285
  this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[row - 9].label}`;
253
286
  this.onNeedsRender?.();
254
287
  return true;
@@ -274,6 +307,12 @@ export class ChatView implements TuiView {
274
307
  this.selectedEffort = this.availableEfforts[this.effortSelectedIndex].id;
275
308
  this.isEffortModalOpen = false;
276
309
  const currentM = this.availableModels[this.selectedModelIndex];
310
+ saveTuiSettings({
311
+ chat: {
312
+ model: currentM,
313
+ effort: this.selectedEffort,
314
+ },
315
+ });
277
316
  this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[this.effortSelectedIndex].label}`;
278
317
  this.onNeedsRender?.();
279
318
  return true;
@@ -7,6 +7,7 @@ import type { TuiView } from "../types.ts";
7
7
  import type { KeyEvent } from "../screen.ts";
8
8
  import { theme, drawBox, stringWidth, truncate, pad, stripAnsi, setClipboardText } from "../theme.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
+ import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
10
11
 
11
12
  export class LogsView implements TuiView {
12
13
  public readonly id = "logs";
@@ -14,6 +15,21 @@ export class LogsView implements TuiView {
14
15
  public readonly tabNumber = 6;
15
16
 
16
17
  private filter: "all" | "warn" | "error" = "all";
18
+
19
+ constructor() {
20
+ const saved = loadTuiSettings();
21
+ if (saved.logs?.filter && ["all", "warn", "error"].includes(saved.logs.filter)) {
22
+ this.filter = saved.logs.filter;
23
+ }
24
+ }
25
+
26
+ private setFilter(newFilter: "all" | "warn" | "error"): void {
27
+ this.filter = newFilter;
28
+ this.scrollOffset = 0;
29
+ this.selectedLogIndex = null;
30
+ saveTuiSettings({ logs: { filter: newFilter } });
31
+ }
32
+
17
33
  private scrollOffset = 0; // 0 = at the bottom (follow newest)
18
34
  private hoveredChip: "all" | "warn" | "error" | "copy" | "clear" | null = null;
19
35
  private selectedLogIndex: number | null = null;
@@ -103,21 +119,15 @@ export class LogsView implements TuiView {
103
119
  for (const c of chips) {
104
120
  if (col >= c.startCol && col <= c.endCol) {
105
121
  if (c.id === "all") {
106
- this.filter = "all";
107
- this.scrollOffset = 0;
108
- this.selectedLogIndex = null;
122
+ this.setFilter("all");
109
123
  return true;
110
124
  }
111
125
  if (c.id === "warn") {
112
- this.filter = "warn";
113
- this.scrollOffset = 0;
114
- this.selectedLogIndex = null;
126
+ this.setFilter("warn");
115
127
  return true;
116
128
  }
117
129
  if (c.id === "error") {
118
- this.filter = "error";
119
- this.scrollOffset = 0;
120
- this.selectedLogIndex = null;
130
+ this.setFilter("error");
121
131
  return true;
122
132
  }
123
133
  if (c.id === "copy") {
@@ -202,21 +212,15 @@ export class LogsView implements TuiView {
202
212
 
203
213
  // Filter toggles
204
214
  if ((key.name === "t" || key.name === "T") && !key.ctrl) {
205
- this.filter = "all";
206
- this.scrollOffset = 0;
207
- this.selectedLogIndex = null;
215
+ this.setFilter("all");
208
216
  return true;
209
217
  }
210
218
  if ((key.name === "w" || key.name === "W") && !key.ctrl) {
211
- this.filter = "warn";
212
- this.scrollOffset = 0;
213
- this.selectedLogIndex = null;
219
+ this.setFilter("warn");
214
220
  return true;
215
221
  }
216
222
  if ((key.name === "e" || key.name === "E") && !key.ctrl) {
217
- this.filter = "error";
218
- this.scrollOffset = 0;
219
- this.selectedLogIndex = null;
223
+ this.setFilter("error");
220
224
  return true;
221
225
  }
222
226