@link-assistant/hive-mind 2.1.3 → 2.1.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - acc3ea1: Simplify the `/limits` output with subscription headings and hide unused Codex limits and credits.
8
+
3
9
  ## 2.1.3
4
10
 
5
11
  ### Patch Changes
package/README.hi.md CHANGED
@@ -521,7 +521,9 @@ Shows:
521
521
  - RAM usage (used vs total)
522
522
  - Disk space usage
523
523
  - GitHub API rate limits
524
- - Claude usage limits (session and weekly)
524
+ - Claude and ChatGPT subscription usage windows
525
+ - Non-zero Codex weekly limits and credits
526
+ - Solve queue status
525
527
  ```
526
528
 
527
529
  #### `/terminal_watch` - Live Session Log
package/README.md CHANGED
@@ -542,7 +542,9 @@ Shows:
542
542
  - RAM usage (used vs total)
543
543
  - Disk space usage
544
544
  - GitHub API rate limits
545
- - Claude usage limits (session and weekly)
545
+ - Claude and ChatGPT subscription usage windows
546
+ - Non-zero Codex weekly limits and credits
547
+ - Solve queue status
546
548
  ```
547
549
 
548
550
  #### `/terminal_watch` - Live Session Log
package/README.ru.md CHANGED
@@ -522,7 +522,9 @@ Shows:
522
522
  - RAM usage (used vs total)
523
523
  - Disk space usage
524
524
  - GitHub API rate limits
525
- - Claude usage limits (session and weekly)
525
+ - Claude and ChatGPT subscription usage windows
526
+ - Non-zero Codex weekly limits and credits
527
+ - Solve queue status
526
528
  ```
527
529
 
528
530
  #### `/terminal_watch` — Live Session Log
package/README.zh.md CHANGED
@@ -518,7 +518,9 @@ Shows:
518
518
  - RAM usage (used vs total)
519
519
  - Disk space usage
520
520
  - GitHub API rate limits
521
- - Claude usage limits (session and weekly)
521
+ - Claude and ChatGPT subscription usage windows
522
+ - Non-zero Codex weekly limits and credits
523
+ - Solve queue status
522
524
  ```
523
525
 
524
526
  #### `/terminal_watch` - Live Session Log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -9,12 +9,15 @@ const ENGLISH_LIMITS = {
9
9
  balance: 'balance',
10
10
  claude_5_hour_session: 'Claude 5 hour session',
11
11
  claude_limits: 'Claude limits',
12
+ claude_subscription_title: 'Claude{{plan}} subscription',
13
+ chatgpt_subscription_title: 'ChatGPT{{plan}} subscription',
12
14
  codex_5_hour_session: 'Codex 5 hour session',
13
15
  codex_credits: 'Codex credits',
14
16
  codex_limits: 'Codex limits',
15
17
  cpu: 'CPU',
16
18
  cpu_cores_used: 'CPU cores used',
17
19
  current_time: 'Current time',
20
+ current_week: 'Current week',
18
21
  current_week_all_models: 'Current week (all models)',
19
22
  current_week_sonnet_only: 'Current week (Sonnet only)',
20
23
  disabled_by_admin: '`--show-limits` is disabled by the bot administrator.',
@@ -25,6 +28,7 @@ const ENGLISH_LIMITS = {
25
28
  duration_second_short: 's',
26
29
  end: 'End',
27
30
  five_hour_session: '5h session',
31
+ five_hour_limit_session: '5 hour session',
28
32
  five_min_load_avg: '5m load avg',
29
33
  github_api: 'GitHub API',
30
34
  limits_at_end: 'Limits at end',
@@ -76,6 +80,10 @@ const ENGLISH_LIMITS = {
76
80
  start: 'Start',
77
81
  subscription_ends: 'Subscription ends {{time}}',
78
82
  subscription_ends_in: 'Subscription ends in {{duration}} ({{time}})',
83
+ subscription_detail_ends: 'ends {{time}}',
84
+ subscription_detail_ends_in: 'ends in {{duration}}; {{time}}',
85
+ subscription_detail_trial_ends: 'trial ends {{time}}',
86
+ subscription_detail_trial_ends_in: 'trial ends in {{duration}}; {{time}}',
79
87
  subscription_status: 'Subscription: {{status}}',
80
88
  trial_ends: 'Trial ends {{time}}',
81
89
  trial_ends_in: 'Trial ends in {{duration}} ({{time}})',
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { CACHE_TTL, DEFAULT_CODEX_AUTH_PATH, DEFAULT_CREDENTIALS_PATH, decodeJwtPayload, getLimitCache, readCodexAuth, readCredentials } from './limits.lib.mjs';
13
- import { formatLocalizedRelativeTime, formatLocalizedResetTime, formatSubscriptionEnds, formatSubscriptionStatus, formatTrialEnds, resolveLimitLocale } from './limits-i18n.lib.mjs';
13
+ import { formatLocalizedRelativeTime, formatLocalizedResetTime, formatSubscriptionEnds, formatSubscriptionStatus, formatTrialEnds, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
14
14
 
15
15
  const PROFILE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/profile';
16
16
 
@@ -32,6 +32,45 @@ export function formatSubscriptionLines(subscription, options = {}) {
32
32
  return line ? `${line}\n` : '';
33
33
  }
34
34
 
35
+ function humanizePlanType(planType, provider) {
36
+ if (!planType) return '';
37
+ const providerPrefix = provider === 'claude' ? /^claude[\s_-]*/i : /^(chatgpt|codex|openai)[\s_-]*/i;
38
+ const normalized = String(planType).trim().replace(providerPrefix, '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
39
+ if (!normalized) return '';
40
+ return normalized
41
+ .split(' ')
42
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
43
+ .join(' ');
44
+ }
45
+
46
+ function formatSubscriptionDetail(subscription, options = {}) {
47
+ if (!subscription) return '';
48
+ const locale = resolveLimitLocale(options);
49
+ const buildDetail = (iso, inKey, atKey) => {
50
+ const resetTime = formatLocalizedResetTime(iso, true, { locale });
51
+ if (!resetTime) return '';
52
+ const duration = formatLocalizedRelativeTime(iso, { locale });
53
+ return duration ? lt(inKey, { duration, time: resetTime }, { locale }) : lt(atKey, { time: resetTime }, { locale });
54
+ };
55
+
56
+ if (subscription.endsAt) return buildDetail(subscription.endsAt, 'subscription_detail_ends_in', 'subscription_detail_ends');
57
+ if (subscription.trialEndsAt) return buildDetail(subscription.trialEndsAt, 'subscription_detail_trial_ends_in', 'subscription_detail_trial_ends');
58
+ return subscription.status ? String(subscription.status).trim() : '';
59
+ }
60
+
61
+ export function formatSubscriptionHeading(provider, subscription, options = {}) {
62
+ const locale = resolveLimitLocale(options);
63
+ const normalizedProvider = provider === 'claude' ? 'claude' : 'chatgpt';
64
+ const planType = subscription?.planType || options?.planType || null;
65
+ if (!subscription && !planType) return '';
66
+
67
+ const plan = humanizePlanType(planType, normalizedProvider);
68
+ const titleKey = normalizedProvider === 'claude' ? 'claude_subscription_title' : 'chatgpt_subscription_title';
69
+ const title = lt(titleKey, { plan: plan ? ` ${plan}` : '' }, { locale });
70
+ const detail = formatSubscriptionDetail(subscription, { locale });
71
+ return detail ? `${title} (${detail})` : title;
72
+ }
73
+
35
74
  /**
36
75
  * Get Claude subscription metadata.
37
76
  *
@@ -14,7 +14,7 @@ import utc from 'dayjs/plugin/utc.js';
14
14
 
15
15
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
16
16
  import { formatLimitResetsAt, formatLimitResetsIn, formatLocalizedCurrentTime, formatLocalizedRelativeTime, formatLocalizedResetTime, localizeCompactDuration, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
17
- import { formatSubscriptionLines, getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo } from './limits-subscription.lib.mjs';
17
+ import { formatSubscriptionHeading, formatSubscriptionLines, getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo } from './limits-subscription.lib.mjs';
18
18
  export { getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo };
19
19
  // Initialize dayjs plugins
20
20
  dayjs.extend(utc);
@@ -280,6 +280,61 @@ function getLocalizedRelativeReset(window, options = {}, fallbackRelative = null
280
280
  return formatRelativeTime(window?.resetsAt, options) || localizeCompactDuration(fallbackRelative, options);
281
281
  }
282
282
 
283
+ function formatCodeBlock(content) {
284
+ const text = Array.isArray(content) ? content.join('\n') : String(content ?? '');
285
+ return '```\n' + (text.endsWith('\n') ? text : `${text}\n`) + '```';
286
+ }
287
+
288
+ function formatPlainTitledCodeSection(section) {
289
+ const text = String(section ?? '').trimEnd();
290
+ if (!text) return '';
291
+ if (text.includes('```')) return text;
292
+
293
+ const lines = text.split('\n');
294
+ const title = lines.shift();
295
+ const body = lines.join('\n');
296
+ if (!body) return formatCodeBlock(title);
297
+ return `${title}\n\n${formatCodeBlock(body)}`;
298
+ }
299
+
300
+ function formatLimitWindowSection(label, window, periodHours, threshold, options = {}) {
301
+ const locale = resolveLimitLocale(options);
302
+ let section = `${label}\n`;
303
+ if (hasLimitPercentage(window)) {
304
+ const timePassed = calculateTimePassedPercentage(window.resetsAt, periodHours);
305
+ if (timePassed !== null) {
306
+ section += `${getProgressBar(timePassed)} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
307
+ }
308
+
309
+ const pct = Math.floor(window.percentage);
310
+ const bar = getProgressBar(pct, threshold);
311
+ const suffix = pct >= threshold ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
312
+ section += `${bar} ${pct}%${suffix}\n`;
313
+
314
+ const resetTime = getLocalizedResetTime(window, { locale });
315
+ if (resetTime) {
316
+ const relativeTime = getLocalizedRelativeReset(window, { locale });
317
+ section += relativeTime ? `${formatLimitResetsIn(relativeTime, resetTime, { locale })}\n` : `${formatLimitResetsAt(resetTime, { locale })}\n`;
318
+ }
319
+ } else {
320
+ section += `${lt('na', {}, { locale })}\n`;
321
+ }
322
+ return section;
323
+ }
324
+
325
+ function hasPositivePercentage(value) {
326
+ if (value === null || value === undefined) return false;
327
+ const numeric = Number(value);
328
+ return Number.isFinite(numeric) && numeric > 0;
329
+ }
330
+
331
+ function hasPositiveCreditBalance(credits) {
332
+ if (!credits) return false;
333
+ if (credits.unlimited) return true;
334
+ const numeric = Number.parseFloat(String(credits.balance ?? '0'));
335
+ return Number.isFinite(numeric) && numeric > 0;
336
+ }
337
+
283
338
  /**
284
339
  * Get GitHub API rate limits by calling gh api rate_limit
285
340
  * Returns rate limit info for core, search, graphql, and other resources
@@ -1061,118 +1116,51 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1061
1116
  sections.push(section);
1062
1117
  }
1063
1118
 
1064
- // Claude limits section
1065
- // When there's an error (e.g., auth expired), show it once and skip empty subsections
1119
+ const claudeHeading = formatSubscriptionHeading('claude', subscription, { locale });
1120
+ const useShortClaudeLabels = Boolean(claudeHeading);
1121
+ const claudeSections = [];
1122
+
1123
+ // Claude limits section. When there's an error (e.g., auth expired), show it once and skip empty subsections.
1066
1124
  if (claudeError) {
1067
- sections.push(`${lt('claude_limits', {}, { locale })}\n${claudeError}\n`);
1125
+ claudeSections.push(useShortClaudeLabels ? `${claudeError}\n` : `${lt('claude_limits', {}, { locale })}\n${claudeError}\n`);
1068
1126
  } else {
1069
- // Claude 5 hour session (five_hour)
1070
- // Threshold: One-at-a-time mode when usage >= 65%
1071
- let sessionSection = `${lt('claude_5_hour_session', {}, { locale })}\n`;
1072
- if (hasLimitPercentage(usage?.currentSession)) {
1073
- const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
1074
- if (timePassed !== null) {
1075
- const timeBar = getProgressBar(timePassed);
1076
- sessionSection += `${timeBar} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1077
- }
1078
-
1079
- // Use Math.floor so 100% only appears when usage is exactly 100%
1080
- // See: https://github.com/link-assistant/hive-mind/issues/1133
1081
- const pct = Math.floor(usage.currentSession.percentage);
1082
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION);
1083
- const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1084
- sessionSection += `${bar} ${pct}%${suffix}\n`;
1085
-
1086
- const sessionResetTime = getLocalizedResetTime(usage.currentSession, { locale });
1087
- if (sessionResetTime) {
1088
- const relativeTime = getLocalizedRelativeReset(usage.currentSession, { locale });
1089
- if (relativeTime) {
1090
- sessionSection += `${formatLimitResetsIn(relativeTime, sessionResetTime, { locale })}\n`;
1091
- } else {
1092
- sessionSection += `${formatLimitResetsAt(sessionResetTime, { locale })}\n`;
1093
- }
1094
- }
1095
- } else {
1096
- sessionSection += `${lt('na', {}, { locale })}\n`;
1127
+ const hasSonnetOnly = hasLimitPercentage(usage?.sonnetOnly);
1128
+ claudeSections.push(formatLimitWindowSection(useShortClaudeLabels ? lt('five_hour_limit_session', {}, { locale }) : lt('claude_5_hour_session', {}, { locale }), usage?.currentSession, 5, DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION, { locale }));
1129
+ claudeSections.push(formatLimitWindowSection(useShortClaudeLabels && !hasSonnetOnly ? lt('current_week', {}, { locale }) : lt('current_week_all_models', {}, { locale }), usage?.allModels, 168, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY, { locale }));
1130
+ if (hasSonnetOnly || !useShortClaudeLabels) {
1131
+ claudeSections.push(formatLimitWindowSection(lt('current_week_sonnet_only', {}, { locale }), usage?.sonnetOnly, 168, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY, { locale }));
1097
1132
  }
1098
- sections.push(sessionSection);
1099
-
1100
- // Current week (all models / seven_day)
1101
- // Threshold: One-at-a-time mode when usage >= 97%
1102
- let allModelsSection = `${lt('current_week_all_models', {}, { locale })}\n`;
1103
- if (hasLimitPercentage(usage?.allModels)) {
1104
- const timePassed = calculateTimePassedPercentage(usage.allModels.resetsAt, 168);
1105
- if (timePassed !== null) {
1106
- const timeBar = getProgressBar(timePassed);
1107
- allModelsSection += `${timeBar} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1108
- }
1109
1133
 
1110
- // Use Math.floor so 100% only appears when usage is exactly 100%
1111
- // See: https://github.com/link-assistant/hive-mind/issues/1133
1112
- const pct = Math.floor(usage.allModels.percentage);
1113
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
1114
- const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1115
- allModelsSection += `${bar} ${pct}%${suffix}\n`;
1116
-
1117
- const allModelsResetTime = getLocalizedResetTime(usage.allModels, { locale });
1118
- if (allModelsResetTime) {
1119
- const relativeTime = getLocalizedRelativeReset(usage.allModels, { locale });
1120
- if (relativeTime) {
1121
- allModelsSection += `${formatLimitResetsIn(relativeTime, allModelsResetTime, { locale })}\n`;
1122
- } else {
1123
- allModelsSection += `${formatLimitResetsAt(allModelsResetTime, { locale })}\n`;
1124
- }
1125
- }
1126
- } else {
1127
- allModelsSection += `${lt('na', {}, { locale })}\n`;
1134
+ if (!useShortClaudeLabels) {
1135
+ const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1136
+ if (subscriptionLines) claudeSections.push(subscriptionLines);
1128
1137
  }
1129
- sections.push(allModelsSection);
1130
-
1131
- // Current week (Sonnet only / seven_day_sonnet)
1132
- // Threshold: One-at-a-time mode when usage >= 97% (same as all models)
1133
- let sonnetSection = `${lt('current_week_sonnet_only', {}, { locale })}\n`;
1134
- if (hasLimitPercentage(usage?.sonnetOnly)) {
1135
- // Add time passed progress bar first (no threshold marker for time)
1136
- const timePassed = calculateTimePassedPercentage(usage.sonnetOnly.resetsAt, 168);
1137
- if (timePassed !== null) {
1138
- const timeBar = getProgressBar(timePassed);
1139
- sonnetSection += `${timeBar} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1140
- }
1138
+ }
1141
1139
 
1142
- // Add usage progress bar second with threshold marker
1143
- // Use Math.floor so 100% only appears when usage is exactly 100%
1144
- // See: https://github.com/link-assistant/hive-mind/issues/1133
1145
- const pct = Math.floor(usage.sonnetOnly.percentage);
1146
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
1147
- const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1148
- sonnetSection += `${bar} ${pct}%${suffix}\n`;
1149
-
1150
- const sonnetResetTime = getLocalizedResetTime(usage.sonnetOnly, { locale });
1151
- if (sonnetResetTime) {
1152
- const relativeTime = getLocalizedRelativeReset(usage.sonnetOnly, { locale });
1153
- if (relativeTime) {
1154
- sonnetSection += `${formatLimitResetsIn(relativeTime, sonnetResetTime, { locale })}\n`;
1155
- } else {
1156
- sonnetSection += `${formatLimitResetsAt(sonnetResetTime, { locale })}\n`;
1157
- }
1158
- }
1159
- } else {
1160
- sonnetSection += `${lt('na', {}, { locale })}\n`;
1140
+ const hasFencedExtraSection = extraSections.some(extra => String(extra ?? '').includes('```'));
1141
+ const useSplitLayout = Boolean(claudeHeading) || hasFencedExtraSection;
1142
+
1143
+ if (!useSplitLayout) {
1144
+ sections.push(...claudeSections);
1145
+ for (const extra of extraSections) {
1146
+ sections.push(extra);
1161
1147
  }
1162
- sections.push(sonnetSection);
1148
+ return formatCodeBlock(sections.join('\n'));
1149
+ }
1163
1150
 
1164
- const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1165
- if (subscriptionLines) sections.push(subscriptionLines);
1151
+ const markdownSections = [];
1152
+ markdownSections.push(formatCodeBlock(claudeHeading ? sections.join('\n') : [...sections, ...claudeSections].join('\n')));
1153
+ if (claudeHeading) {
1154
+ markdownSections.push(claudeHeading);
1155
+ markdownSections.push(formatCodeBlock(claudeSections.join('\n')));
1166
1156
  }
1167
1157
 
1168
- // Append any caller-provided extra sections (e.g. queue status) inside the code block
1169
1158
  for (const extra of extraSections) {
1170
- sections.push(extra);
1159
+ const formatted = formatPlainTitledCodeSection(extra);
1160
+ if (formatted) markdownSections.push(formatted);
1171
1161
  }
1172
1162
 
1173
- // Wrap all sections in a single code block for monospace font / aligned progress bars.
1174
- // Sections are separated by blank lines; the trailing newline on each section provides spacing.
1175
- return '```\n' + sections.join('\n') + '```';
1163
+ return markdownSections.join('\n\n');
1176
1164
  }
1177
1165
 
1178
1166
  /**
@@ -1185,64 +1173,33 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1185
1173
  */
1186
1174
  export function formatCodexLimitsSection(codexLimits, codexError = null, options = {}) {
1187
1175
  const locale = resolveLimitLocale(options);
1188
- if (codexError) {
1189
- return `${lt('codex_limits', {}, { locale })}\n${codexError}\n`;
1190
- }
1191
-
1176
+ const subscription = options?.subscription || null;
1192
1177
  const usage = codexLimits?.usage || null;
1193
1178
  const additionalRateLimits = codexLimits?.additionalRateLimits || [];
1194
1179
  const credits = codexLimits?.credits || null;
1195
- const planType = codexLimits?.planType || null;
1196
- const subscription = options?.subscription || null;
1180
+ const planType = subscription?.planType || codexLimits?.planType || null;
1181
+ const heading = formatSubscriptionHeading('codex', subscription, { locale, planType });
1182
+ const useTitledLayout = Boolean(heading);
1197
1183
 
1198
- let section = `${lt('codex_limits', {}, { locale })}\n`;
1199
- if (planType) {
1200
- section += `${lt('plan', {}, { locale })}: ${planType}\n`;
1184
+ if (codexError) {
1185
+ const errorSection = useTitledLayout ? `${codexError}\n` : `${lt('codex_limits', {}, { locale })}\n${codexError}\n`;
1186
+ return useTitledLayout ? `${heading}\n\n${formatCodeBlock(errorSection)}` : errorSection;
1201
1187
  }
1202
1188
 
1203
- let sessionSection = `${lt('codex_5_hour_session', {}, { locale })}\n`;
1204
- if (hasLimitPercentage(usage?.currentSession)) {
1205
- const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
1206
- if (timePassed !== null) {
1207
- sessionSection += `${getProgressBar(timePassed)} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1208
- }
1209
- const pct = Math.floor(usage.currentSession.percentage);
1210
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION);
1211
- const suffix = pct >= DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1212
- sessionSection += `${bar} ${pct}%${suffix}\n`;
1213
- const sessionResetTime = getLocalizedResetTime(usage.currentSession, { locale });
1214
- if (sessionResetTime) {
1215
- const relativeTime = getLocalizedRelativeReset(usage.currentSession, { locale });
1216
- sessionSection += relativeTime ? `${formatLimitResetsIn(relativeTime, sessionResetTime, { locale })}\n` : `${formatLimitResetsAt(sessionResetTime, { locale })}\n`;
1217
- }
1218
- } else {
1219
- sessionSection += `${lt('na', {}, { locale })}\n`;
1189
+ let section = useTitledLayout ? '' : `${lt('codex_limits', {}, { locale })}\n`;
1190
+ if (planType && !useTitledLayout) {
1191
+ section += `${lt('plan', {}, { locale })}: ${planType}\n`;
1220
1192
  }
1221
1193
 
1222
- let weeklySection = `${lt('current_week_all_models', {}, { locale })}\n`;
1223
- if (hasLimitPercentage(usage?.allModels)) {
1224
- const timePassed = calculateTimePassedPercentage(usage.allModels.resetsAt, 168);
1225
- if (timePassed !== null) {
1226
- weeklySection += `${getProgressBar(timePassed)} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1227
- }
1228
- const pct = Math.floor(usage.allModels.percentage);
1229
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CODEX_WEEKLY);
1230
- const suffix = pct >= DISPLAY_THRESHOLDS.CODEX_WEEKLY ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1231
- weeklySection += `${bar} ${pct}%${suffix}\n`;
1232
- const weeklyResetTime = getLocalizedResetTime(usage.allModels, { locale });
1233
- if (weeklyResetTime) {
1234
- const relativeTime = getLocalizedRelativeReset(usage.allModels, { locale });
1235
- weeklySection += relativeTime ? `${formatLimitResetsIn(relativeTime, weeklyResetTime, { locale })}\n` : `${formatLimitResetsAt(weeklyResetTime, { locale })}\n`;
1236
- }
1237
- } else {
1238
- weeklySection += `${lt('na', {}, { locale })}\n`;
1239
- }
1194
+ const sessionSection = formatLimitWindowSection(useTitledLayout ? lt('five_hour_limit_session', {}, { locale }) : lt('codex_5_hour_session', {}, { locale }), usage?.currentSession, 5, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION, { locale });
1195
+ const weeklySection = formatLimitWindowSection(useTitledLayout ? lt('current_week', {}, { locale }) : lt('current_week_all_models', {}, { locale }), usage?.allModels, 168, DISPLAY_THRESHOLDS.CODEX_WEEKLY, { locale });
1240
1196
 
1241
1197
  section += `${sessionSection}\n${weeklySection}`;
1242
1198
 
1243
- if (additionalRateLimits.length > 0) {
1199
+ const visibleAdditionalRateLimits = additionalRateLimits.filter(limit => hasPositivePercentage(limit.allModels?.percentage));
1200
+ if (visibleAdditionalRateLimits.length > 0) {
1244
1201
  section += `\n${lt('additional_codex_limits', {}, { locale })}\n`;
1245
- for (const limit of additionalRateLimits) {
1202
+ for (const limit of visibleAdditionalRateLimits) {
1246
1203
  const sessionPct = limit.currentSession?.percentage;
1247
1204
  const weeklyPct = limit.allModels?.percentage;
1248
1205
  const sessionText = sessionPct === null || sessionPct === undefined ? `${lt('session', {}, { locale })} ${lt('na', {}, { locale })}` : `${lt('session', {}, { locale })} ${Math.floor(sessionPct)}%`;
@@ -1251,15 +1208,17 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1251
1208
  }
1252
1209
  }
1253
1210
 
1254
- if (credits) {
1211
+ if (hasPositiveCreditBalance(credits)) {
1255
1212
  const creditSummary = credits.unlimited ? lt('unlimited', {}, { locale }) : `${credits.balance ?? '0'} ${lt('balance', {}, { locale })}`;
1256
1213
  section += `\n${lt('codex_credits', {}, { locale })}\n${creditSummary}\n`;
1257
1214
  }
1258
1215
 
1259
- const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1260
- if (subscriptionLines) section += subscriptionLines;
1216
+ if (!useTitledLayout) {
1217
+ const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1218
+ if (subscriptionLines) section += subscriptionLines;
1219
+ }
1261
1220
 
1262
- return section;
1221
+ return useTitledLayout ? `${heading}\n\n${formatCodeBlock(section)}` : section;
1263
1222
  }
1264
1223
 
1265
1224
  // ============================================================================
@@ -192,6 +192,11 @@ en
192
192
  hour
193
193
  session "Claude 5 hour session"
194
194
  limits "Claude limits"
195
+ subscription
196
+ title "Claude{{plan}} subscription"
197
+ chatgpt
198
+ subscription
199
+ title "ChatGPT{{plan}} subscription"
195
200
  codex
196
201
  5
197
202
  hour
@@ -205,6 +210,7 @@ en
205
210
  current
206
211
  time "Current time"
207
212
  week
213
+ label "Current week"
208
214
  all
209
215
  models "Current week (all models)"
210
216
  sonnet
@@ -226,6 +232,8 @@ en
226
232
  end "End"
227
233
  five
228
234
  hour
235
+ limit
236
+ session "5 hour session"
229
237
  session "5h session"
230
238
  min
231
239
  load
@@ -314,6 +322,14 @@ en
314
322
  session "session"
315
323
  start "Start"
316
324
  subscription
325
+ detail
326
+ ends
327
+ label "ends {{time}}"
328
+ in "ends in {{duration}}; {{time}}"
329
+ trial
330
+ ends
331
+ label "trial ends {{time}}"
332
+ in "trial ends in {{duration}}; {{time}}"
317
333
  ends
318
334
  label "Subscription ends {{time}}"
319
335
  in "Subscription ends in {{duration}} ({{time}})"
@@ -192,6 +192,11 @@ hi
192
192
  hour
193
193
  session "Claude 5 घंटे का सत्र"
194
194
  limits "Claude सीमाएँ"
195
+ subscription
196
+ title "Claude{{plan}} सदस्यता"
197
+ chatgpt
198
+ subscription
199
+ title "ChatGPT{{plan}} सदस्यता"
195
200
  codex
196
201
  5
197
202
  hour
@@ -205,6 +210,7 @@ hi
205
210
  current
206
211
  time "वर्तमान समय"
207
212
  week
213
+ label "वर्तमान सप्ताह"
208
214
  all
209
215
  models "वर्तमान सप्ताह (सभी मॉडल)"
210
216
  sonnet
@@ -226,6 +232,8 @@ hi
226
232
  end "समाप्ति"
227
233
  five
228
234
  hour
235
+ limit
236
+ session "5 घंटे का सत्र"
229
237
  session "5 घंटे का सत्र"
230
238
  min
231
239
  load
@@ -314,6 +322,14 @@ hi
314
322
  session "सत्र"
315
323
  start "शुरुआत"
316
324
  subscription
325
+ detail
326
+ ends
327
+ label "{{time}} को समाप्त होगी"
328
+ in "{{duration}} में समाप्त होगी; {{time}}"
329
+ trial
330
+ ends
331
+ label "ट्रायल {{time}} को समाप्त होगा"
332
+ in "ट्रायल {{duration}} में समाप्त होगा; {{time}}"
317
333
  ends
318
334
  label "सदस्यता समाप्त होगी {{time}}"
319
335
  in "सदस्यता {{duration}} में समाप्त होगी ({{time}})"
@@ -192,6 +192,11 @@ ru
192
192
  hour
193
193
  session "5-часовой сеанс Claude"
194
194
  limits "Лимиты Claude"
195
+ subscription
196
+ title "Подписка Claude{{plan}}"
197
+ chatgpt
198
+ subscription
199
+ title "Подписка ChatGPT{{plan}}"
195
200
  codex
196
201
  5
197
202
  hour
@@ -205,6 +210,7 @@ ru
205
210
  current
206
211
  time "Текущее время"
207
212
  week
213
+ label "Текущая неделя"
208
214
  all
209
215
  models "Текущая неделя (все модели)"
210
216
  sonnet
@@ -226,6 +232,8 @@ ru
226
232
  end "Конец"
227
233
  five
228
234
  hour
235
+ limit
236
+ session "5-часовой сеанс"
229
237
  session "5-часовой сеанс"
230
238
  min
231
239
  load
@@ -314,6 +322,14 @@ ru
314
322
  session "сеанс"
315
323
  start "Начало"
316
324
  subscription
325
+ detail
326
+ ends
327
+ label "заканчивается {{time}}"
328
+ in "заканчивается через {{duration}}; {{time}}"
329
+ trial
330
+ ends
331
+ label "пробный период заканчивается {{time}}"
332
+ in "пробный период заканчивается через {{duration}}; {{time}}"
317
333
  ends
318
334
  label "Подписка заканчивается {{time}}"
319
335
  in "Подписка заканчивается через {{duration}} ({{time}})"
@@ -192,6 +192,11 @@ zh
192
192
  hour
193
193
  session "Claude 5 小时会话"
194
194
  limits "Claude 限额"
195
+ subscription
196
+ title "Claude{{plan}} 订阅"
197
+ chatgpt
198
+ subscription
199
+ title "ChatGPT{{plan}} 订阅"
195
200
  codex
196
201
  5
197
202
  hour
@@ -205,6 +210,7 @@ zh
205
210
  current
206
211
  time "当前时间"
207
212
  week
213
+ label "本周"
208
214
  all
209
215
  models "本周(所有模型)"
210
216
  sonnet
@@ -226,6 +232,8 @@ zh
226
232
  end "结束"
227
233
  five
228
234
  hour
235
+ limit
236
+ session "5 小时会话"
229
237
  session "5 小时会话"
230
238
  min
231
239
  load
@@ -314,6 +322,14 @@ zh
314
322
  session "会话"
315
323
  start "开始"
316
324
  subscription
325
+ detail
326
+ ends
327
+ label "结束于 {{time}}"
328
+ in "将在 {{duration}} 后结束;{{time}}"
329
+ trial
330
+ ends
331
+ label "试用结束于 {{time}}"
332
+ in "试用将在 {{duration}} 后结束;{{time}}"
317
333
  ends
318
334
  label "订阅结束于 {{time}}"
319
335
  in "订阅将在 {{duration}} 后结束 ({{time}})"