qwenproxy-cli 1.0.13 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -24,8 +24,8 @@
24
24
  "reset": "npx tsx src/reset-cooldowns.ts",
25
25
  "purge": "npx tsx src/delete-chats.ts",
26
26
  "test": "npm run test:mock && npm run test:live",
27
- "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts",
28
- "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts",
27
+ "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
28
+ "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
29
29
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
30
30
  "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
31
31
  "typecheck": "tsc --noEmit"
package/src/api/server.ts CHANGED
@@ -650,15 +650,20 @@ export async function startServer(options?: {
650
650
  const BATCH_SIZE = config.playwright.initBatchSize;
651
651
 
652
652
  if (accounts.length > 0) {
653
- let readyAccountId: string | null = null;
654
653
  const totalAccounts = accounts.length;
655
654
 
656
655
  // Warm accounts in priority order (recently successful accounts first),
657
- // skipping accounts still on cooldown, so the startup account matches
658
- // the one request routing will pick first.
656
+ // skipping accounts still on cooldown. Warm up to maxActiveContexts
657
+ // accounts (default 2: main + reserve) so an immediate failover has a
658
+ // live browser ready instead of incurring a cold start.
659
659
  const warmOrder = getAccountsByPriority(accounts).filter(
660
660
  (account) => !getAccountCooldownInfo(account.id),
661
661
  );
662
+ const targetWarmCount = Math.min(
663
+ warmOrder.length,
664
+ Math.max(1, config.playwright.maxActiveContexts),
665
+ );
666
+ const readyAccountIds = new Set<string>();
662
667
 
663
668
  for (let i = 0; i < warmOrder.length; i++) {
664
669
  const ok = await prepareAccountRuntime(
@@ -669,24 +674,26 @@ export async function startServer(options?: {
669
674
  warmQwenChatPool,
670
675
  );
671
676
  if (ok) {
677
+ readyAccountIds.add(warmOrder[i].id);
672
678
  console.log(
673
- `✅ [Server] Account ready (${i + 1}/${totalAccounts}): ${maskEmail(warmOrder[i].email)}`,
679
+ `✅ [Server] Account ready (${readyAccountIds.size}/${totalAccounts}): ${maskEmail(warmOrder[i].email)}`,
674
680
  );
675
- readyAccountId = warmOrder[i].id;
676
- break;
681
+ if (readyAccountIds.size >= targetWarmCount) {
682
+ break;
683
+ }
677
684
  }
678
685
  }
679
686
 
680
687
  const remainingAccounts = accounts.filter(
681
- (account) => account.id !== readyAccountId,
688
+ (account) => !readyAccountIds.has(account.id),
682
689
  );
683
- if (readyAccountId === null) {
690
+ if (readyAccountIds.size === 0) {
684
691
  console.warn(
685
692
  `⚠️ [Server] No account ready during startup; continuing in background`,
686
693
  );
687
694
  }
688
695
 
689
- if (config.playwright.prepareAllOnStartup || readyAccountId === null) {
696
+ if (config.playwright.prepareAllOnStartup || readyAccountIds.size === 0) {
690
697
  if (config.playwright.prepareAllOnStartup && remainingAccounts.length > 0) {
691
698
  console.log(
692
699
  `🪶 [Server] Preparing ${remainingAccounts.length} standby account(s) in background`,
@@ -35,7 +35,7 @@ import {
35
35
  type AccountLease,
36
36
  } from "../../core/account-concurrency.ts";
37
37
  import { isAuthMockEnabled } from "../../services/auth-playwright.ts";
38
- import { refreshHeaders } from "../../services/playwright.ts";
38
+ import { isPlaywrightInitialized, refreshHeaders } from "../../services/playwright.ts";
39
39
  import {
40
40
  clearAllSessionsForAccount,
41
41
  createQwenStream,
@@ -77,7 +77,18 @@ const MAX_ANTI_BOT_ROTATIONS = 1;
77
77
  * a stuck account page (closed context / WAF) can otherwise hold each browser
78
78
  * op for 60s and keep the personalization mutex blocked for minutes.
79
79
  */
80
- const PERSONALIZATION_SYNC_DEADLINE_MS = 30_000;
80
+ export const PERSONALIZATION_SYNC_DEADLINE_MS = 30_000;
81
+ export const COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS = 60_000;
82
+
83
+ export function computePersonalizationDeadlineMs(
84
+ accountId: string | undefined,
85
+ navigationTimeoutMs = config.timeouts.navigation,
86
+ ): number {
87
+ if (accountId && accountId !== "global" && isPlaywrightInitialized(accountId)) {
88
+ return PERSONALIZATION_SYNC_DEADLINE_MS;
89
+ }
90
+ return Math.max(COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS, navigationTimeoutMs);
91
+ }
81
92
 
82
93
  /**
83
94
  * Hard deadline for a single stream-acquire attempt (models sync + truncation
@@ -1183,15 +1194,16 @@ async function tryCreateStreamWithRetry(
1183
1194
  return false;
1184
1195
  },
1185
1196
  );
1197
+ const syncDeadlineMs = computePersonalizationDeadlineMs(currentAccountId);
1186
1198
  personalizationApplied = await Promise.race([
1187
1199
  syncPromise,
1188
1200
  new Promise<boolean>((resolve) => {
1189
1201
  personalizationDeadlineTimer = setTimeout(() => {
1190
1202
  if (!syncSettled) {
1191
- syncFailure = `sync timed out after ${PERSONALIZATION_SYNC_DEADLINE_MS}ms`;
1203
+ syncFailure = `sync timed out after ${syncDeadlineMs}ms`;
1192
1204
  }
1193
1205
  resolve(false);
1194
- }, PERSONALIZATION_SYNC_DEADLINE_MS);
1206
+ }, syncDeadlineMs);
1195
1207
  }),
1196
1208
  ]);
1197
1209
  // The sync won the race: stop the deadline so it cannot keep the
@@ -3157,6 +3157,12 @@ export function registerPlaywrightAccountForTests(
3157
3157
  lastAccountActivity.set(accountId, lastActivityAt);
3158
3158
  }
3159
3159
 
3160
+ export function unregisterPlaywrightAccountForTests(accountId: string): void {
3161
+ accountPages.delete(accountId);
3162
+ accountContexts.delete(accountId);
3163
+ lastAccountActivity.delete(accountId);
3164
+ }
3165
+
3160
3166
  // ─── Token TTL Diagnostics ───────────────────────────────────────────────────
3161
3167
 
3162
3168
  export interface CookieDiagnostic {
@@ -145,7 +145,7 @@ export class AccountsView implements TuiView {
145
145
  public async handleKey(key: KeyEvent): Promise<boolean | void> {
146
146
  // 0. Confirm Dialog Active
147
147
  if (this.confirmDialog) {
148
- if (key.name === "s" || key.name === "S" || key.name === "enter" || key.name === "return") {
148
+ if (key.name === "s" || key.name === "S") {
149
149
  const dialog = this.confirmDialog;
150
150
  this.confirmDialog = null;
151
151
  this.confirmDialogHovered = null;
@@ -158,17 +158,40 @@ export class AccountsView implements TuiView {
158
158
  this.setStatusMessage(theme.muted("Ação cancelada"));
159
159
  return true;
160
160
  }
161
+ if (key.name === "enter" || key.name === "return") {
162
+ if (this.confirmDialogHovered === "cancel") {
163
+ this.confirmDialog = null;
164
+ this.confirmDialogHovered = null;
165
+ this.setStatusMessage(theme.muted("Ação cancelada"));
166
+ return true;
167
+ }
168
+ const dialog = this.confirmDialog;
169
+ this.confirmDialog = null;
170
+ this.confirmDialogHovered = null;
171
+ await dialog.onConfirm();
172
+ return true;
173
+ }
174
+ if (key.name === "left" || key.name === "right" || key.name === "tab") {
175
+ this.confirmDialogHovered = this.confirmDialogHovered === "cancel" ? "confirm" : "cancel";
176
+ return true;
177
+ }
161
178
  if (key.name === "hover" && key.mouse) {
162
179
  const { row, col } = key.mouse;
163
- const btnRow = this.lastConfirmModalStartRow + 4;
164
- if (row === btnRow) {
165
- const startCol = this.lastConfirmModalLeftPad + 2;
166
- if (col >= startCol && col <= startCol + 25) {
167
- this.confirmDialogHovered = "confirm";
180
+ const btnRow = (this.lastConfirmModalStartRow || 4) + 5;
181
+ if (row === btnRow || row === btnRow - 1) {
182
+ const relCol = col - (this.lastConfirmModalLeftPad || 0);
183
+ if (relCol >= 2 && relCol <= 34) {
184
+ if (this.confirmDialogHovered !== "confirm") {
185
+ this.confirmDialogHovered = "confirm";
186
+ return true;
187
+ }
168
188
  return true;
169
189
  }
170
- if (col >= startCol + 26 && col <= startCol + 50) {
171
- this.confirmDialogHovered = "cancel";
190
+ if (relCol >= 35 && relCol <= 60) {
191
+ if (this.confirmDialogHovered !== "cancel") {
192
+ this.confirmDialogHovered = "cancel";
193
+ return true;
194
+ }
172
195
  return true;
173
196
  }
174
197
  }
@@ -179,17 +202,17 @@ export class AccountsView implements TuiView {
179
202
  }
180
203
  if (key.name === "click" && key.mouse) {
181
204
  const { row, col } = key.mouse;
182
- const btnRow = this.lastConfirmModalStartRow + 4;
183
- if (row === btnRow) {
184
- const startCol = this.lastConfirmModalLeftPad + 2;
185
- if (col >= startCol && col <= startCol + 25) {
205
+ const btnRow = (this.lastConfirmModalStartRow || 4) + 5;
206
+ if (row === btnRow || row === btnRow - 1) {
207
+ const relCol = col - (this.lastConfirmModalLeftPad || 0);
208
+ if (relCol >= 2 && relCol <= 34) {
186
209
  const dialog = this.confirmDialog;
187
210
  this.confirmDialog = null;
188
211
  this.confirmDialogHovered = null;
189
212
  await dialog.onConfirm();
190
213
  return true;
191
214
  }
192
- if (col >= startCol + 26 && col <= startCol + 50) {
215
+ if (relCol >= 35 && relCol <= 60) {
193
216
  this.confirmDialog = null;
194
217
  this.confirmDialogHovered = null;
195
218
  this.setStatusMessage(theme.muted("Ação cancelada"));
@@ -804,14 +827,15 @@ export class AccountsView implements TuiView {
804
827
  if (this.confirmDialog) {
805
828
  const modalW = Math.min(width - 4, 66);
806
829
  this.lastConfirmModalLeftPad = Math.max(0, Math.floor((width - modalW) / 2));
830
+ this.lastConfirmModalStartRow = 4;
807
831
  const confirmBtn =
808
832
  this.confirmDialogHovered === "confirm"
809
833
  ? theme.bgHover(theme.red(" [ S / Enter ] Sim, Confirmar "))
810
- : theme.red("[ S / Enter ] Sim, Confirmar");
834
+ : ` ${theme.red("[ S / Enter ] Sim, Confirmar")} `;
811
835
  const cancelBtn =
812
836
  this.confirmDialogHovered === "cancel"
813
837
  ? theme.bgHover(theme.green(" [ N / Esc ] Cancelar "))
814
- : theme.green("[ N / Esc ] Cancelar");
838
+ : ` ${theme.green("[ N / Esc ] Cancelar")} `;
815
839
 
816
840
  const modalContent = [
817
841
  "",
@@ -126,7 +126,7 @@ export class StorageView implements TuiView {
126
126
  public async handleKey(key: KeyEvent): Promise<boolean | void> {
127
127
  // 0. Confirm Dialog Active
128
128
  if (this.confirmDialog) {
129
- if (key.name === "s" || key.name === "S" || key.name === "enter" || key.name === "return") {
129
+ if (key.name === "s" || key.name === "S") {
130
130
  const dialog = this.confirmDialog;
131
131
  this.confirmDialog = null;
132
132
  this.confirmDialogHovered = null;
@@ -139,17 +139,40 @@ export class StorageView implements TuiView {
139
139
  this.addLog(theme.muted("Ação cancelada"));
140
140
  return true;
141
141
  }
142
+ if (key.name === "enter" || key.name === "return") {
143
+ if (this.confirmDialogHovered === "cancel") {
144
+ this.confirmDialog = null;
145
+ this.confirmDialogHovered = null;
146
+ this.addLog(theme.muted("Ação cancelada"));
147
+ return true;
148
+ }
149
+ const dialog = this.confirmDialog;
150
+ this.confirmDialog = null;
151
+ this.confirmDialogHovered = null;
152
+ await dialog.onConfirm();
153
+ return true;
154
+ }
155
+ if (key.name === "left" || key.name === "right" || key.name === "tab") {
156
+ this.confirmDialogHovered = this.confirmDialogHovered === "cancel" ? "confirm" : "cancel";
157
+ return true;
158
+ }
142
159
  if (key.name === "hover" && key.mouse) {
143
160
  const { row, col } = key.mouse;
144
- const btnRow = this.lastConfirmModalStartRow + 4;
145
- if (row === btnRow) {
146
- const startCol = this.lastConfirmModalLeftPad + 2;
147
- if (col >= startCol && col <= startCol + 25) {
148
- this.confirmDialogHovered = "confirm";
161
+ const btnRow = (this.lastConfirmModalStartRow || 4) + 5;
162
+ if (row === btnRow || row === btnRow - 1) {
163
+ const relCol = col - (this.lastConfirmModalLeftPad || 0);
164
+ if (relCol >= 2 && relCol <= 34) {
165
+ if (this.confirmDialogHovered !== "confirm") {
166
+ this.confirmDialogHovered = "confirm";
167
+ return true;
168
+ }
149
169
  return true;
150
170
  }
151
- if (col >= startCol + 26 && col <= startCol + 50) {
152
- this.confirmDialogHovered = "cancel";
171
+ if (relCol >= 35 && relCol <= 60) {
172
+ if (this.confirmDialogHovered !== "cancel") {
173
+ this.confirmDialogHovered = "cancel";
174
+ return true;
175
+ }
153
176
  return true;
154
177
  }
155
178
  }
@@ -160,17 +183,17 @@ export class StorageView implements TuiView {
160
183
  }
161
184
  if (key.name === "click" && key.mouse) {
162
185
  const { row, col } = key.mouse;
163
- const btnRow = this.lastConfirmModalStartRow + 4;
164
- if (row === btnRow) {
165
- const startCol = this.lastConfirmModalLeftPad + 2;
166
- if (col >= startCol && col <= startCol + 25) {
186
+ const btnRow = (this.lastConfirmModalStartRow || 4) + 5;
187
+ if (row === btnRow || row === btnRow - 1) {
188
+ const relCol = col - (this.lastConfirmModalLeftPad || 0);
189
+ if (relCol >= 2 && relCol <= 34) {
167
190
  const dialog = this.confirmDialog;
168
191
  this.confirmDialog = null;
169
192
  this.confirmDialogHovered = null;
170
193
  await dialog.onConfirm();
171
194
  return true;
172
195
  }
173
- if (col >= startCol + 26 && col <= startCol + 50) {
196
+ if (relCol >= 35 && relCol <= 60) {
174
197
  this.confirmDialog = null;
175
198
  this.confirmDialogHovered = null;
176
199
  this.addLog(theme.muted("Ação cancelada"));
@@ -409,14 +432,15 @@ export class StorageView implements TuiView {
409
432
  if (this.confirmDialog) {
410
433
  const modalW = Math.min(width - 4, 66);
411
434
  this.lastConfirmModalLeftPad = Math.max(0, Math.floor((width - modalW) / 2));
435
+ this.lastConfirmModalStartRow = 4;
412
436
  const confirmBtn =
413
437
  this.confirmDialogHovered === "confirm"
414
438
  ? theme.bgHover(theme.red(" [ S / Enter ] Sim, Confirmar "))
415
- : theme.red("[ S / Enter ] Sim, Confirmar");
439
+ : ` ${theme.red("[ S / Enter ] Sim, Confirmar")} `;
416
440
  const cancelBtn =
417
441
  this.confirmDialogHovered === "cancel"
418
442
  ? theme.bgHover(theme.green(" [ N / Esc ] Cancelar "))
419
- : theme.green("[ N / Esc ] Cancelar");
443
+ : ` ${theme.green("[ N / Esc ] Cancelar")} `;
420
444
 
421
445
  const modalContent = [
422
446
  "",