@triflux/core 10.1.0 → 10.25.1

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.
Files changed (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +254 -33
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +831 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -18
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +167 -38
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +285 -111
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +87 -13
  75. package/hub/research.mjs +33 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +127 -61
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +72 -71
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +391 -247
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +70 -10
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +32 -7
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
package/hud/renderers.mjs CHANGED
@@ -5,26 +5,51 @@ import { existsSync, readdirSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import {
8
- dim, bold, green, red, yellow, cyan,
9
- claudeOrange, codexWhite, geminiBlue,
10
- colorByPercent, colorByProvider,
11
- CLAUDE_ORANGE, CODEX_WHITE, GEMINI_BLUE,
8
+ bold,
9
+ CLAUDE_ORANGE,
10
+ CODEX_WHITE,
11
+ claudeOrange,
12
+ codexWhite,
13
+ colorByPercent,
14
+ colorByProvider,
15
+ dim,
16
+ GEMINI_BLUE,
17
+ geminiBlue,
18
+ green,
19
+ red,
20
+ yellow,
12
21
  } from "./colors.mjs";
13
22
  import {
14
- PROVIDER_PREFIX_WIDTH, ACCOUNT_LABEL_WIDTH,
15
- FIVE_HOUR_MS, SEVEN_DAY_MS, ONE_DAY_MS,
16
- TEAM_STATE_PATH, SV_ACCUMULATOR_PATH, LEGACY_SV_ACCUMULATOR,
23
+ ACCOUNT_LABEL_WIDTH,
24
+ FIVE_HOUR_MS,
25
+ LEGACY_SV_ACCUMULATOR,
26
+ ONE_DAY_MS,
27
+ PROVIDER_PREFIX_WIDTH,
28
+ SEVEN_DAY_MS,
29
+ SV_ACCUMULATOR_PATH,
30
+ TEAM_STATE_PATH,
17
31
  } from "./constants.mjs";
32
+ import { buildContextUsageView } from "./context-monitor.mjs";
33
+ import { deriveGeminiLimits } from "./providers/gemini.mjs";
34
+ import { getTerminalColumns, tierBar, tierDimBar } from "./terminal.mjs";
18
35
  import {
19
- readJson, readJsonMigrate, stripAnsi, padAnsiRight, fitText, truncateAnsi,
20
- clampPercent, formatPercentCell, formatPlaceholderPercentCell,
21
- formatTimeCell, formatTimeCellDH,
22
- formatResetRemaining, formatResetRemainingDayHour,
23
- formatTokenCount, formatSvPct, formatSavings,
36
+ clampPercent,
37
+ fitText,
38
+ formatPercentCell,
39
+ formatPlaceholderPercentCell,
40
+ formatResetRemaining,
41
+ formatResetRemainingDayHour,
42
+ formatSavings,
43
+ formatSvPct,
44
+ formatTimeCell,
45
+ formatTimeCellDH,
46
+ formatTokenCount,
47
+ padAnsiRight,
48
+ readJson,
49
+ readJsonMigrate,
50
+ stripAnsi,
51
+ truncateAnsi,
24
52
  } from "./utils.mjs";
25
- import { tierBar, tierDimBar, getTerminalColumns } from "./terminal.mjs";
26
- import { deriveGeminiLimits } from "./providers/gemini.mjs";
27
- import { buildContextUsageView } from "./context-monitor.mjs";
28
53
 
29
54
  // ============================================================================
30
55
  // 최근 벤치마크 diff 파일 읽기
@@ -33,15 +58,26 @@ export function readLatestBenchmarkDiff() {
33
58
  const diffsDir = join(homedir(), ".omc", "state", "cx-auto-tokens", "diffs");
34
59
  if (!existsSync(diffsDir)) return null;
35
60
  try {
36
- const files = readdirSync(diffsDir).filter(f => f.endsWith(".json")).sort().reverse();
61
+ const files = readdirSync(diffsDir)
62
+ .filter((f) => f.endsWith(".json"))
63
+ .sort()
64
+ .reverse();
37
65
  if (files.length === 0) return null;
38
66
  return readJson(join(diffsDir, files[0]), null);
39
- } catch { return null; }
67
+ } catch {
68
+ return null;
69
+ }
40
70
  }
41
71
 
42
72
  // 토큰 절약액 누적치 읽기 (tfx-auto token tracker)
43
73
  export function readTokenSavings() {
44
- const savingsPath = join(homedir(), ".omc", "state", "tfx-auto-tokens", "savings-total.json");
74
+ const savingsPath = join(
75
+ homedir(),
76
+ ".omc",
77
+ "state",
78
+ "tfx-auto-tokens",
79
+ "savings-total.json",
80
+ );
45
81
  const data = readJson(savingsPath, null);
46
82
  if (!data || data.totalSaved === 0) return null;
47
83
  return data;
@@ -63,14 +99,15 @@ export function formatTokenSummary(diff) {
63
99
  const inputStr = formatTokenCount(t.input);
64
100
  const outputStr = formatTokenCount(t.output);
65
101
  const actualStr = formatSavings(s.actualCost);
66
- const claudeStr = formatSavings(s.claudeCost);
67
- const savedPct = s.claudeCost > 0
68
- ? Math.round((s.saved / s.claudeCost) * 100)
69
- : 0;
102
+ const _claudeStr = formatSavings(s.claudeCost);
103
+ const savedPct =
104
+ s.claudeCost > 0 ? Math.round((s.saved / s.claudeCost) * 100) : 0;
70
105
 
71
- return `${dim("tok:")}${inputStr}${dim("in")} ${outputStr}${dim("out")} ` +
106
+ return (
107
+ `${dim("tok:")}${inputStr}${dim("in")} ${outputStr}${dim("out")} ` +
72
108
  `${dim("cost:")}${actualStr} ` +
73
- `${dim("sv:")}${green(formatSavings(s.saved))}${dim("(")}${savedPct}%${dim(")")}`;
109
+ `${dim("sv:")}${green(formatSavings(s.saved))}${dim("(")}${savedPct}%${dim(")")}`
110
+ );
74
111
  }
75
112
 
76
113
  // ============================================================================
@@ -78,10 +115,14 @@ export function formatTokenSummary(diff) {
78
115
  // ============================================================================
79
116
  export function getTeamRow(currentTier) {
80
117
  const teamState = readJson(TEAM_STATE_PATH, null);
81
- if (!teamState || !teamState.sessionName) return null;
118
+ if (!teamState?.sessionName) return null;
82
119
 
83
120
  // 팀 생존 확인: startedAt 기준 24시간 초과면 stale로 간주
84
- if (teamState.startedAt && (Date.now() - teamState.startedAt) > 24 * 60 * 60 * 1000) return null;
121
+ if (
122
+ teamState.startedAt &&
123
+ Date.now() - teamState.startedAt > 24 * 60 * 60 * 1000
124
+ )
125
+ return null;
85
126
 
86
127
  const workers = (teamState.members || []).filter((m) => m.role === "worker");
87
128
  if (!workers.length) return null;
@@ -92,26 +133,44 @@ export function getTeamRow(currentTier) {
92
133
  const total = tasks.length || workers.length;
93
134
 
94
135
  // 경과 시간 (80col 이상에서만 표시)
95
- const elapsed = (teamState.startedAt && (currentTier === "full" || currentTier === "compact"))
96
- ? `${Math.round((Date.now() - teamState.startedAt) / 60000)}m`
97
- : "";
136
+ const elapsed =
137
+ teamState.startedAt && (currentTier === "full" || currentTier === "compact")
138
+ ? `${Math.round((Date.now() - teamState.startedAt) / 60000)}m`
139
+ : "";
98
140
 
99
141
  // CLI 브랜드: 단일문자 + ANSI 색상 (x=codex, g=gemini, c=claude)
100
- const cliTag = (cli) => cli === "codex" ? bold(codexWhite("x")) : cli === "gemini" ? bold(geminiBlue("g")) : bold(claudeOrange("c"));
142
+ const cliTag = (cli) =>
143
+ cli === "codex"
144
+ ? bold(codexWhite("x"))
145
+ : cli === "gemini"
146
+ ? bold(geminiBlue("g"))
147
+ : bold(claudeOrange("c"));
101
148
  // 멤버 상태: 태그 + 상태기호 (60col 이상)
102
- const memberIcons = (currentTier === "full" || currentTier === "compact" || currentTier === "minimal") ? workers.map((m) => {
103
- const task = tasks.find((t) => t.owner === m.name);
104
- const status = task?.status === "completed" ? green("\u2713")
105
- : task?.status === "in_progress" ? yellow("\u22EF")
106
- : task?.status === "failed" ? red("\u2717")
107
- : dim("\u25CC");
108
- return `${cliTag(m.cli)}${status}`;
109
- }).join(" ") : "";
149
+ const memberIcons =
150
+ currentTier === "full" ||
151
+ currentTier === "compact" ||
152
+ currentTier === "minimal"
153
+ ? workers
154
+ .map((m) => {
155
+ const task = tasks.find((t) => t.owner === m.name);
156
+ const status =
157
+ task?.status === "completed"
158
+ ? green("\u2713")
159
+ : task?.status === "in_progress"
160
+ ? yellow("\u22EF")
161
+ : task?.status === "failed"
162
+ ? red("\u2717")
163
+ : dim("\u25CC");
164
+ return `${cliTag(m.cli)}${status}`;
165
+ })
166
+ .join(" ")
167
+ : "";
110
168
 
111
169
  // 진행 텍스트
112
- const doneText = failed > 0
113
- ? `${completed}/${total} ${red(`${failed}\u2717`)}`
114
- : `${completed}/${total}`;
170
+ const doneText =
171
+ failed > 0
172
+ ? `${completed}/${total} ${red(`${failed}\u2717`)}`
173
+ : `${completed}/${total}`;
115
174
 
116
175
  const leftText = elapsed ? `${doneText} ${dim(elapsed)}` : doneText;
117
176
 
@@ -122,13 +181,50 @@ export function getTeamRow(currentTier) {
122
181
  };
123
182
  }
124
183
 
184
+ // ============================================================================
185
+ // Mission Board 렌더러
186
+ // ============================================================================
187
+
188
+ const STATUS_ICON = {
189
+ active: "*",
190
+ idle: ".",
191
+ done: "+",
192
+ failed: "!",
193
+ };
194
+
195
+ /**
196
+ * Mission Board 상태를 1줄 compact 포맷으로 렌더링한다.
197
+ * 예: "MB: exec:+ ui:* perf:. [2/3 67%]"
198
+ * @param {{ agents: Array<{name: string, status: string, progress: number}>, dagLevel: number, totalProgress: number } | null} state
199
+ * @returns {string}
200
+ */
201
+ export function renderMissionBoard(state) {
202
+ if (!state) return "";
203
+
204
+ const { agents, totalProgress } = state;
205
+ const done = agents.filter((a) => a.status === "done").length;
206
+ const total = agents.length;
207
+
208
+ const parts = agents.map((a) => {
209
+ const icon = STATUS_ICON[a.status] ?? "?";
210
+ return `${a.name}:${icon}`;
211
+ });
212
+
213
+ return `MB: ${parts.join(" ")} [${done}/${total} ${totalProgress}%]`;
214
+ }
215
+
125
216
  // ============================================================================
126
217
  // 행 정렬 렌더링
127
218
  // ============================================================================
128
219
  export function renderAlignedRows(rows) {
129
220
  const cols = getTerminalColumns() || 120;
130
- const rightRows = rows.filter((row) => stripAnsi(String(row.right || "")).trim().length > 0);
131
- const rawLeftWidth = rightRows.reduce((max, row) => Math.max(max, stripAnsi(row.left).length), 0);
221
+ const rightRows = rows.filter(
222
+ (row) => stripAnsi(String(row.right || "")).trim().length > 0,
223
+ );
224
+ const rawLeftWidth = rightRows.reduce(
225
+ (max, row) => Math.max(max, stripAnsi(row.left).length),
226
+ 0,
227
+ );
132
228
  return rows.map((row) => {
133
229
  const prefix = padAnsiRight(row.prefix, PROVIDER_PREFIX_WIDTH);
134
230
  const hasRight = stripAnsi(String(row.right || "")).trim().length > 0;
@@ -147,9 +243,10 @@ export function renderAlignedRows(rows) {
147
243
  return truncateAnsi(`${prefix} ${left}`, cols);
148
244
  }
149
245
  const rightVisible = stripAnsi(row.right).length;
150
- const right = rightVisible <= availableForRight
151
- ? row.right
152
- : truncateAnsi(row.right, availableForRight);
246
+ const right =
247
+ rightVisible <= availableForRight
248
+ ? row.right
249
+ : truncateAnsi(row.right, availableForRight);
153
250
  return `${prefix} ${left} ${dim("|")} ${right}`;
154
251
  });
155
252
  }
@@ -157,23 +254,43 @@ export function renderAlignedRows(rows) {
157
254
  // ============================================================================
158
255
  // micro tier: 모든 프로바이더를 1줄로 압축
159
256
  // ============================================================================
160
- export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSession, geminiBucket, combinedSvPct) {
257
+ export function getMicroLine(
258
+ contextView,
259
+ claudeUsage,
260
+ codexBuckets,
261
+ geminiSession,
262
+ geminiBucket,
263
+ combinedSvPct,
264
+ geminiMarker = "g",
265
+ ) {
161
266
  const ctxView = contextView || buildContextUsageView({}, null);
162
-
163
267
  // Claude 5h/1w
164
- const cF = claudeUsage?.fiveHourPercent != null ? clampPercent(claudeUsage.fiveHourPercent) : null;
165
- const cW = claudeUsage?.weeklyPercent != null ? clampPercent(claudeUsage.weeklyPercent) : null;
166
- const cVal = claudeUsage != null
167
- ? `${cF != null ? colorByProvider(cF, `${cF}`, claudeOrange) : dim("--")}${dim("/")}${cW != null ? colorByProvider(cW, `${cW}`, claudeOrange) : dim("--")}`
168
- : dim("--/--");
268
+ const cF =
269
+ claudeUsage?.fiveHourPercent != null
270
+ ? clampPercent(claudeUsage.fiveHourPercent)
271
+ : null;
272
+ const cW =
273
+ claudeUsage?.weeklyPercent != null
274
+ ? clampPercent(claudeUsage.weeklyPercent)
275
+ : null;
276
+ const cVal =
277
+ claudeUsage != null
278
+ ? `${cF != null ? colorByProvider(cF, `${cF}`, claudeOrange) : dim("--")}${dim("/")}${cW != null ? colorByProvider(cW, `${cW}`, claudeOrange) : dim("--")}`
279
+ : dim("--/--");
169
280
 
170
281
  // Codex 5h/1w
171
282
  let xVal = dim("--/--");
172
283
  if (codexBuckets) {
173
284
  const mb = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
174
285
  if (mb) {
175
- const xF = mb.primary?.used_percent != null ? clampPercent(mb.primary.used_percent) : null;
176
- const xW = mb.secondary?.used_percent != null ? clampPercent(mb.secondary.used_percent) : null;
286
+ const xF =
287
+ mb.primary?.used_percent != null
288
+ ? clampPercent(mb.primary.used_percent)
289
+ : null;
290
+ const xW =
291
+ mb.secondary?.used_percent != null
292
+ ? clampPercent(mb.secondary.used_percent)
293
+ : null;
177
294
  xVal = `${xF != null ? colorByProvider(xF, `${xF}`, codexWhite) : dim("--")}${dim("/")}${xW != null ? colorByProvider(xW, `${xW}`, codexWhite) : dim("--")}`;
178
295
  }
179
296
  }
@@ -182,7 +299,9 @@ export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSessi
182
299
  let gVal;
183
300
  if (geminiBucket) {
184
301
  const gl = deriveGeminiLimits(geminiBucket);
185
- const gU = gl ? gl.usedPct : clampPercent((1 - (geminiBucket.remainingFraction ?? 1)) * 100);
302
+ const gU = gl
303
+ ? gl.usedPct
304
+ : clampPercent((1 - (geminiBucket.remainingFraction ?? 1)) * 100);
186
305
  gVal = colorByProvider(gU, `${gU}`, geminiBlue);
187
306
  } else if ((geminiSession?.total || 0) > 0) {
188
307
  gVal = geminiBlue("\u221E");
@@ -194,9 +313,10 @@ export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSessi
194
313
  const sv = formatSvPct(combinedSvPct || 0).trim();
195
314
 
196
315
  const cols = getTerminalColumns() || 120;
197
- const line = `${bold(claudeOrange("c"))}${dim(":")}${cVal} ` +
316
+ const line =
317
+ `${bold(claudeOrange("c"))}${dim(":")}${cVal} ` +
198
318
  `${bold(codexWhite("x"))}${dim(":")}${xVal} ` +
199
- `${bold(geminiBlue("g"))}${dim(":")}${gVal} ` +
319
+ `${bold(geminiBlue(geminiMarker))}${dim(":")}${gVal} ` +
200
320
  `${dim("sv:")}${sv} ` +
201
321
  `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
202
322
  return truncateAnsi(line, cols);
@@ -205,10 +325,14 @@ export function getMicroLine(contextView, claudeUsage, codexBuckets, geminiSessi
205
325
  // ============================================================================
206
326
  // Claude 행 렌더러
207
327
  // ============================================================================
208
- export function getClaudeRows(currentTier, contextView, claudeUsage, combinedSvPct) {
328
+ export function getClaudeRows(
329
+ currentTier,
330
+ contextView,
331
+ claudeUsage,
332
+ combinedSvPct,
333
+ ) {
209
334
  const ctxView = contextView || buildContextUsageView({}, null);
210
335
  const prefix = `${bold(claudeOrange("c"))}:`;
211
-
212
336
  // 절약 퍼센트
213
337
  const svStr = formatSvPct(combinedSvPct || 0);
214
338
  const svSuffix = `${dim("sv:")}${svStr}`;
@@ -225,36 +349,67 @@ export function getClaudeRows(currentTier, contextView, claudeUsage, combinedSvP
225
349
 
226
350
  const hasData = claudeUsage != null;
227
351
 
228
- const fStr = hasData && fiveHourPercent != null ? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange) : dim(formatPlaceholderPercentCell());
229
- const wStr = hasData && weeklyPercent != null ? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange) : dim(formatPlaceholderPercentCell());
230
- const fBar = hasData && fiveHourPercent != null ? tierBar(currentTier, fiveHourPercent, CLAUDE_ORANGE) : tierDimBar(currentTier);
231
- const wBar = hasData && weeklyPercent != null ? tierBar(currentTier, weeklyPercent, CLAUDE_ORANGE) : tierDimBar(currentTier);
352
+ const fStr =
353
+ hasData && fiveHourPercent != null
354
+ ? colorByProvider(
355
+ fiveHourPercent,
356
+ formatPercentCell(fiveHourPercent),
357
+ claudeOrange,
358
+ )
359
+ : dim(formatPlaceholderPercentCell());
360
+ const wStr =
361
+ hasData && weeklyPercent != null
362
+ ? colorByProvider(
363
+ weeklyPercent,
364
+ formatPercentCell(weeklyPercent),
365
+ claudeOrange,
366
+ )
367
+ : dim(formatPlaceholderPercentCell());
368
+ const fBar =
369
+ hasData && fiveHourPercent != null
370
+ ? tierBar(currentTier, fiveHourPercent, CLAUDE_ORANGE)
371
+ : tierDimBar(currentTier);
372
+ const wBar =
373
+ hasData && weeklyPercent != null
374
+ ? tierBar(currentTier, weeklyPercent, CLAUDE_ORANGE)
375
+ : tierDimBar(currentTier);
232
376
  const fTime = formatTimeCell(fiveHourReset);
233
377
  const wTime = formatTimeCellDH(weeklyReset);
234
378
 
235
379
  if (currentTier === "nano" || currentTier === "micro") {
236
- const fShort = hasData && fiveHourPercent != null ? colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange) : dim("--");
237
- const wShort = hasData && weeklyPercent != null ? colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange) : dim("--");
380
+ const fShort =
381
+ hasData && fiveHourPercent != null
382
+ ? colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange)
383
+ : dim("--");
384
+ const wShort =
385
+ hasData && weeklyPercent != null
386
+ ? colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange)
387
+ : dim("--");
238
388
  const quotaSection = `${fShort}${dim("/")}${wShort}`;
239
389
  return [{ prefix, left: quotaSection, right: "" }];
240
390
  }
241
391
 
242
392
  if (currentTier === "minimal") {
243
- const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}`;
393
+ const staleTag = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
394
+ const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleTag}`;
244
395
  const right = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
245
396
  return [{ prefix, left: quotaSection, right }];
246
397
  }
247
398
 
248
399
  if (currentTier === "compact") {
249
400
  const quotaSection = `${dim("5h:")}${fStr} ${dim(fTime)} ${dim("1w:")}${wStr} ${dim(wTime)}`;
250
- const warning = ctxView.warningTag ? ` ${dim("|")} ${yellow(ctxView.warningTag)}` : "";
401
+ const warning = ctxView.warningTag
402
+ ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
403
+ : "";
251
404
  const contextSection = `${svSuffix} ${dim("|")} ${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${warning}`;
252
405
  return [{ prefix, left: quotaSection, right: contextSection }];
253
406
  }
254
407
 
255
408
  // full tier (>= 120 cols)
256
409
  const quotaSection = `${dim("5h:")}${fBar}${fStr} ${dim(fTime)} ${dim("1w:")}${wBar}${wStr} ${dim(wTime)}`;
257
- const warning = ctxView.warningTag ? ` ${dim("|")} ${yellow(ctxView.warningTag)}` : "";
410
+ const warning = ctxView.warningTag
411
+ ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
412
+ : "";
258
413
  const contextSection = `${svSuffix} ${dim("|")} ${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${warning}`;
259
414
  return [{ prefix, left: quotaSection, right: contextSection }];
260
415
  }
@@ -262,53 +417,109 @@ export function getClaudeRows(currentTier, contextView, claudeUsage, combinedSvP
262
417
  // ============================================================================
263
418
  // 계정 라벨 + 범용 프로바이더 행 렌더러
264
419
  // ============================================================================
265
- export function getAccountLabel(provider, accountsConfig, accountsState, codexEmail) {
420
+ export function getAccountLabel(
421
+ provider,
422
+ accountsConfig,
423
+ accountsState,
424
+ codexEmail,
425
+ ) {
266
426
  const providerConfig = accountsConfig?.providers?.[provider] || [];
267
427
  const providerState = accountsState?.providers?.[provider] || {};
268
428
  const lastId = providerState.last_selected_id;
269
- const picked = providerConfig.find((a) => a.id === lastId) || providerConfig[0]
270
- || { id: `${provider}-main`, label: provider };
429
+ const picked = providerConfig.find((a) => a.id === lastId) ||
430
+ providerConfig[0] || { id: `${provider}-main`, label: provider };
271
431
  let label = picked.label || picked.id;
272
432
  if (codexEmail) label = codexEmail;
273
433
  if (label.includes("@")) label = label.split("@")[0];
274
434
  return label;
275
435
  }
276
436
 
277
- export function getProviderRow(currentTier, provider, marker, markerColor, qosProfile, accountsConfig, accountsState, realQuota, codexEmail, savingsMultiplier, modelLabel) {
278
- const accountLabel = fitText(getAccountLabel(provider, accountsConfig, accountsState, codexEmail), ACCOUNT_LABEL_WIDTH);
437
+ export function getProviderRow(
438
+ currentTier,
439
+ provider,
440
+ marker,
441
+ markerColor,
442
+ qosProfile,
443
+ accountsConfig,
444
+ accountsState,
445
+ realQuota,
446
+ codexEmail,
447
+ savingsMultiplier,
448
+ modelLabel,
449
+ ) {
450
+ const accountLabel = fitText(
451
+ getAccountLabel(provider, accountsConfig, accountsState, codexEmail),
452
+ ACCOUNT_LABEL_WIDTH,
453
+ );
279
454
 
280
455
  // 절약 퍼센트 섹션
281
- const svPct = savingsMultiplier != null ? Math.round(savingsMultiplier * 100) : null;
456
+ const svPct =
457
+ savingsMultiplier != null ? Math.round(savingsMultiplier * 100) : null;
282
458
  const svStr = formatSvPct(svPct);
283
- const modelLabelStr = modelLabel ? ` ${markerColor(modelLabel)}` : "";
459
+ const _modelLabelStr = modelLabel ? ` ${markerColor(modelLabel)}` : "";
284
460
 
285
461
  // 프로바이더별 색상 프로필
286
- const provAnsi = provider === "codex" ? CODEX_WHITE : provider === "gemini" ? GEMINI_BLUE : GREEN;
287
- const provFn = provider === "codex" ? codexWhite : provider === "gemini" ? geminiBlue : green;
462
+ const isGeminiFamily = provider === "gemini" || provider === "antigravity";
463
+ const provAnsi =
464
+ provider === "codex" ? CODEX_WHITE : isGeminiFamily ? GEMINI_BLUE : GREEN;
465
+ const provFn =
466
+ provider === "codex" ? codexWhite : isGeminiFamily ? geminiBlue : green;
288
467
 
289
468
  let quotaSection;
290
- let extraRightSection = "";
469
+ const _extraRightSection = "";
291
470
 
292
471
  if (currentTier === "nano" || currentTier === "micro") {
293
472
  const minPrefix = `${bold(markerColor(`${marker}`))}:`;
294
473
  if (realQuota?.type === "codex") {
295
- const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
474
+ const main =
475
+ realQuota.buckets.codex ||
476
+ realQuota.buckets[Object.keys(realQuota.buckets)[0]];
296
477
  if (main) {
297
- const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
298
- const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
299
- const fCellN = fiveP != null ? colorByProvider(fiveP, `${fiveP}%`, provFn) : dim("--%");
300
- const wCellN = weekP != null ? colorByProvider(weekP, `${weekP}%`, provFn) : dim("--%");
301
- return { prefix: minPrefix, left: `${fCellN}${dim("/")}${wCellN}`, right: "" };
478
+ const fiveP =
479
+ main.primary?.used_percent != null
480
+ ? clampPercent(main.primary.used_percent)
481
+ : null;
482
+ const weekP =
483
+ main.secondary?.used_percent != null
484
+ ? clampPercent(main.secondary.used_percent)
485
+ : null;
486
+ const fCellN =
487
+ fiveP != null
488
+ ? colorByProvider(fiveP, `${fiveP}%`, provFn)
489
+ : dim("--%");
490
+ const wCellN =
491
+ weekP != null
492
+ ? colorByProvider(weekP, `${weekP}%`, provFn)
493
+ : dim("--%");
494
+ return {
495
+ prefix: minPrefix,
496
+ left: `${fCellN}${dim("/")}${wCellN}`,
497
+ right: "",
498
+ };
302
499
  }
303
500
  }
304
501
  if (realQuota?.type === "gemini") {
305
502
  const pools = realQuota.pools || {};
306
503
  if (pools.pro || pools.flash) {
307
- const pP = pools.pro ? clampPercent(Math.round((1 - (pools.pro.remainingFraction ?? 1)) * 100)) : null;
308
- const pF = pools.flash ? clampPercent(Math.round((1 - (pools.flash.remainingFraction ?? 1)) * 100)) : null;
309
- const pStr = pP != null ? colorByProvider(pP, `${pP}`, provFn) : dim("--");
310
- const fStr = pF != null ? colorByProvider(pF, `${pF}`, provFn) : dim("--");
311
- return { prefix: minPrefix, left: `${pStr}${dim("/")}${fStr}`, right: "" };
504
+ const pP = pools.pro
505
+ ? clampPercent(
506
+ Math.round((1 - (pools.pro.remainingFraction ?? 1)) * 100),
507
+ )
508
+ : null;
509
+ const pF = pools.flash
510
+ ? clampPercent(
511
+ Math.round((1 - (pools.flash.remainingFraction ?? 1)) * 100),
512
+ )
513
+ : null;
514
+ const pStr =
515
+ pP != null ? colorByProvider(pP, `${pP}`, provFn) : dim("--");
516
+ const fStr =
517
+ pF != null ? colorByProvider(pF, `${pF}`, provFn) : dim("--");
518
+ return {
519
+ prefix: minPrefix,
520
+ left: `${pStr}${dim("/")}${fStr}`,
521
+ right: "",
522
+ };
312
523
  }
313
524
  }
314
525
  return { prefix: minPrefix, left: dim("--/--"), right: "" };
@@ -316,12 +527,26 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
316
527
 
317
528
  if (currentTier === "minimal") {
318
529
  if (realQuota?.type === "codex") {
319
- const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
530
+ const main =
531
+ realQuota.buckets.codex ||
532
+ realQuota.buckets[Object.keys(realQuota.buckets)[0]];
320
533
  if (main) {
321
- const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
322
- const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
323
- const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
324
- const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
534
+ const fiveP =
535
+ main.primary?.used_percent != null
536
+ ? clampPercent(main.primary.used_percent)
537
+ : null;
538
+ const weekP =
539
+ main.secondary?.used_percent != null
540
+ ? clampPercent(main.secondary.used_percent)
541
+ : null;
542
+ const fCell =
543
+ fiveP != null
544
+ ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn)
545
+ : dim(formatPlaceholderPercentCell());
546
+ const wCell =
547
+ weekP != null
548
+ ? colorByProvider(weekP, formatPercentCell(weekP), provFn)
549
+ : dim(formatPlaceholderPercentCell());
325
550
  quotaSection = `${dim("5h:")}${fCell} ${dim("1w:")}${wCell}`;
326
551
  }
327
552
  }
@@ -329,9 +554,12 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
329
554
  const pools = realQuota.pools || {};
330
555
  if (pools.pro || pools.flash) {
331
556
  const slot = (bucket, label) => {
332
- if (!bucket) return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())}`;
557
+ if (!bucket)
558
+ return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())}`;
333
559
  const gl = deriveGeminiLimits(bucket);
334
- const usedP = gl ? gl.usedPct : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
560
+ const usedP = gl
561
+ ? gl.usedPct
562
+ : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
335
563
  return `${dim(label + ":")}${colorByProvider(usedP, formatPercentCell(usedP), provFn)}`;
336
564
  };
337
565
  quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
@@ -343,19 +571,42 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
343
571
  quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim("1w:")}${dim(formatPlaceholderPercentCell())}`;
344
572
  }
345
573
  const prefix = `${bold(markerColor(`${marker}`))}:`;
346
- return { prefix, left: quotaSection, right: accountLabel ? markerColor(accountLabel) : "" };
574
+ return {
575
+ prefix,
576
+ left: quotaSection,
577
+ right: accountLabel ? markerColor(accountLabel) : "",
578
+ };
347
579
  }
348
580
 
349
581
  if (currentTier === "compact") {
350
582
  if (realQuota?.type === "codex") {
351
- const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
583
+ const main =
584
+ realQuota.buckets.codex ||
585
+ realQuota.buckets[Object.keys(realQuota.buckets)[0]];
352
586
  if (main) {
353
- const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
354
- const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
355
- const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
356
- const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
357
- const fiveReset = formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
358
- const weekReset = formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) || "n/a";
587
+ const fiveP =
588
+ main.primary?.used_percent != null
589
+ ? clampPercent(main.primary.used_percent)
590
+ : null;
591
+ const weekP =
592
+ main.secondary?.used_percent != null
593
+ ? clampPercent(main.secondary.used_percent)
594
+ : null;
595
+ const fCell =
596
+ fiveP != null
597
+ ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn)
598
+ : dim(formatPlaceholderPercentCell());
599
+ const wCell =
600
+ weekP != null
601
+ ? colorByProvider(weekP, formatPercentCell(weekP), provFn)
602
+ : dim(formatPlaceholderPercentCell());
603
+ const fiveReset =
604
+ formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
605
+ const weekReset =
606
+ formatResetRemainingDayHour(
607
+ main.secondary?.resets_at,
608
+ SEVEN_DAY_MS,
609
+ ) || "n/a";
359
610
  quotaSection = `${dim("5h:")}${fCell} ${dim(formatTimeCell(fiveReset))} ${dim("1w:")}${wCell} ${dim(formatTimeCellDH(weekReset))}`;
360
611
  }
361
612
  }
@@ -364,10 +615,14 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
364
615
  const hasAnyPool = pools.pro || pools.flash;
365
616
  if (hasAnyPool) {
366
617
  const slot = (bucket, label) => {
367
- if (!bucket) return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
618
+ if (!bucket)
619
+ return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
368
620
  const gl = deriveGeminiLimits(bucket);
369
- const usedP = gl ? gl.usedPct : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
370
- const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
621
+ const usedP = gl
622
+ ? gl.usedPct
623
+ : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
624
+ const rstRemaining =
625
+ formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
371
626
  return `${dim(label + ":")}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))}`;
372
627
  };
373
628
  quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
@@ -379,23 +634,52 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
379
634
  quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ${dim("1w:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH("--d--h"))}`;
380
635
  }
381
636
  const prefix = `${bold(markerColor(`${marker}`))}:`;
382
- const compactRight = [svStr ? `${dim("sv:")}${svStr}` : "", accountLabel ? markerColor(accountLabel) : ""].filter(Boolean).join(" ");
637
+ const compactRight = [
638
+ svStr ? `${dim("sv:")}${svStr}` : "",
639
+ accountLabel ? markerColor(accountLabel) : "",
640
+ ]
641
+ .filter(Boolean)
642
+ .join(" ");
383
643
  return { prefix, left: quotaSection, right: compactRight };
384
644
  }
385
645
 
386
646
  // full tier
387
647
  if (realQuota?.type === "codex") {
388
- const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
648
+ const main =
649
+ realQuota.buckets.codex ||
650
+ realQuota.buckets[Object.keys(realQuota.buckets)[0]];
389
651
  if (main) {
390
- const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
391
- const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
392
- const fiveReset = formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
393
- const weekReset = formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) || "n/a";
394
- const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
395
- const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
396
- const fBar = fiveP != null ? tierBar(currentTier, fiveP, provAnsi) : tierDimBar(currentTier);
397
- const wBar = weekP != null ? tierBar(currentTier, weekP, provAnsi) : tierDimBar(currentTier);
398
- quotaSection = `${dim("5h:")}${fBar}${fCell} ` +
652
+ const fiveP =
653
+ main.primary?.used_percent != null
654
+ ? clampPercent(main.primary.used_percent)
655
+ : null;
656
+ const weekP =
657
+ main.secondary?.used_percent != null
658
+ ? clampPercent(main.secondary.used_percent)
659
+ : null;
660
+ const fiveReset =
661
+ formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
662
+ const weekReset =
663
+ formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) ||
664
+ "n/a";
665
+ const fCell =
666
+ fiveP != null
667
+ ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn)
668
+ : dim(formatPlaceholderPercentCell());
669
+ const wCell =
670
+ weekP != null
671
+ ? colorByProvider(weekP, formatPercentCell(weekP), provFn)
672
+ : dim(formatPlaceholderPercentCell());
673
+ const fBar =
674
+ fiveP != null
675
+ ? tierBar(currentTier, fiveP, provAnsi)
676
+ : tierDimBar(currentTier);
677
+ const wBar =
678
+ weekP != null
679
+ ? tierBar(currentTier, weekP, provAnsi)
680
+ : tierDimBar(currentTier);
681
+ quotaSection =
682
+ `${dim("5h:")}${fBar}${fCell} ` +
399
683
  `${dim(formatTimeCell(fiveReset))} ` +
400
684
  `${dim("1w:")}${wBar}${wCell} ` +
401
685
  `${dim(formatTimeCellDH(weekReset))}`;
@@ -412,14 +696,18 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
412
696
  return `${dim(label + ":")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
413
697
  }
414
698
  const gl = deriveGeminiLimits(bucket);
415
- const usedP = gl ? gl.usedPct : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
416
- const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
699
+ const usedP = gl
700
+ ? gl.usedPct
701
+ : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
702
+ const rstRemaining =
703
+ formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
417
704
  return `${dim(label + ":")}${tierBar(currentTier, usedP, provAnsi)}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))}`;
418
705
  };
419
706
 
420
707
  quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
421
708
  } else {
422
- quotaSection = `${dim("Pr:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ` +
709
+ quotaSection =
710
+ `${dim("Pr:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ` +
423
711
  `${dim("Fl:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
424
712
  }
425
713
  }
@@ -433,7 +721,9 @@ export function getProviderRow(currentTier, provider, marker, markerColor, qosPr
433
721
  const accountSection = `${markerColor(accountLabel)}`;
434
722
  const svSection = svStr ? `${dim("sv:")}${svStr}` : "";
435
723
  const modelLabelSection = modelLabel ? markerColor(modelLabel) : "";
436
- const rightParts = [svSection, accountSection, modelLabelSection].filter(Boolean);
724
+ const rightParts = [svSection, accountSection, modelLabelSection].filter(
725
+ Boolean,
726
+ );
437
727
  return {
438
728
  prefix,
439
729
  left: quotaSection,