-
Notifications
You must be signed in to change notification settings - Fork 253
fix(ui): humanize provider retry delay in the banner copy #3402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { test } from 'node:test'; | ||
| import { getConversationCopy } from '../conversation-copy.js'; | ||
|
|
||
| /** | ||
| * A subscription quota window can hand the runtime an hour-scale Retry-After; | ||
| * the banner must count down in humanized d/h/m/s units rather than a raw | ||
| * five-digit second count that reads as a frozen hang. | ||
| */ | ||
| test('providerRetryScheduled humanizes hour-scale delays in both locales', () => { | ||
| const zh = getConversationCopy('zh').messages.providerRetryScheduled; | ||
| const en = getConversationCopy('en').messages.providerRetryScheduled; | ||
|
|
||
| // Short delays keep the familiar seconds-only form. | ||
| assert.equal(zh(1, 2, 10), '1秒后重试(2/10)'); | ||
| assert.equal(en(1, 2, 10), 'Retrying in 1s (2/10)'); | ||
| assert.equal(zh(45, 2, 10), '45秒后重试(2/10)'); | ||
| assert.equal(en(45, 2, 10), 'Retrying in 45s (2/10)'); | ||
|
|
||
| // Minute- and hour-scale delays spell out the units. | ||
| assert.equal(zh(75, 2, 10), '1分 15秒后重试(2/10)'); | ||
| assert.equal(en(75, 2, 10), 'Retrying in 1m 15s (2/10)'); | ||
| assert.equal(zh(16_083, 2, 10), '4小时 28分 3秒后重试(2/10)'); | ||
| assert.equal(en(16_083, 2, 10), 'Retrying in 4h 28m 3s (2/10)'); | ||
| assert.equal(zh(90_061, 2, 10), '1天 1小时 1分 1秒后重试(2/10)'); | ||
| assert.equal(en(90_061, 2, 10), 'Retrying in 1d 1h 1m 1s (2/10)'); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,54 @@ function formatGoalElapsedUnits(elapsedMs: number, units: GoalElapsedUnits): str | |
| return `${Math.floor(hours / 24)}${units.day} ${hours % 24}${units.hour}`; | ||
| } | ||
|
|
||
| /** Wall-clock units for the provider retry countdown, per locale. */ | ||
| interface ProviderRetryDelayUnits { | ||
| second: string; | ||
| minute: string; | ||
| hour: string; | ||
| day: string; | ||
| separator: string; | ||
| } | ||
|
|
||
| const PROVIDER_RETRY_DELAY_UNITS_ZH: ProviderRetryDelayUnits = { | ||
| second: '秒', | ||
| minute: '分', | ||
| hour: '小时', | ||
| day: '天', | ||
| separator: ' ', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Cosmetic, and a native reader should overrule me if they disagree. |
||
| }; | ||
| const PROVIDER_RETRY_DELAY_UNITS_EN: ProviderRetryDelayUnits = { | ||
| second: 's', | ||
| minute: 'm', | ||
| hour: 'h', | ||
| day: 'd', | ||
| separator: ' ', | ||
| }; | ||
|
|
||
| /** | ||
| * Humanized provider retry delay. A subscription quota window can hand the | ||
| * runtime an hour-scale Retry-After; a raw five-digit second count reads as a | ||
| * frozen hang, so the banner counts down in d/h/m/s units that keep moving | ||
| * every second (unlike the goal chip's minute-granularity ladder). | ||
| */ | ||
| function formatProviderRetryDelay(seconds: number, units: ProviderRetryDelayUnits): string { | ||
| let remaining = Math.max(0, Math.floor(seconds)); | ||
| const parts: string[] = []; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Simpler equivalent solution, spelled out: hoist this ladder to a package both Acknowledging the cost honestly: this is a cross-package move, not a local import, so it is more work than it looks. Not a blocker — but with the ladder now written twice, the third copy is the one that will drift. |
||
| for (const [unit, size] of [ | ||
| [units.day, 86_400], | ||
| [units.hour, 3_600], | ||
| [units.minute, 60], | ||
| ] as const) { | ||
| const value = Math.floor(remaining / size); | ||
| if (value > 0) { | ||
| parts.push(`${value}${unit}`); | ||
| remaining %= size; | ||
| } | ||
| } | ||
| if (remaining > 0 || parts.length === 0) parts.push(`${remaining}${units.second}`); | ||
| return parts.join(units.separator); | ||
| } | ||
|
|
||
| export interface ConversationCopy { | ||
| empty: { | ||
| ariaLabel: string; | ||
|
|
@@ -459,7 +507,7 @@ const CONVERSATION_COPY = { | |
| chooseAriaLabel: (label, branch) => branch ? `选择项目:${label},当前分支 ${branch}` : `选择项目:${label}`, | ||
| }, | ||
| messages: { | ||
| you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', workingPhrases: ['正在琢磨…', '正在推敲…', '正在盘算…', '正在钻研…', '正在忙活…', '正在梳理…', '正在打磨…', '正在鼓捣…', '正在酝酿…', '正在攻坚…', '正在权衡…', '正在拾掇…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `${seconds} 秒后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryReason: { network: '网络中断', provider_unavailable: '模型服务暂时不可用', rate_limit: '触发模型速率限制', timeout: '请求超时', unknown: '模型请求失败' }, safeResumePending: '正在验证…', safeResume: '安全恢复', thinking: '深度思考', truncated: '已截断', copied: '已复制', copying: '复制中', copyFailed: '复制失败', copy: '复制', editMessage: '编辑并重发', editMessageDisabledRunning: '当前回答仍在进行中,结束后再编辑', editMessageDisabledAttachments: '包含附件的历史消息暂不支持编辑并重发', editMessageDisabledQuotes: '包含引用的历史消息暂不支持编辑并重发', editMessageDisabledTransformedText: '通过显式技能发送的历史消息暂不支持编辑并重发', | ||
| you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', workingPhrases: ['正在琢磨…', '正在推敲…', '正在盘算…', '正在钻研…', '正在忙活…', '正在梳理…', '正在打磨…', '正在鼓捣…', '正在酝酿…', '正在攻坚…', '正在权衡…', '正在拾掇…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatProviderRetryDelay(seconds, PROVIDER_RETRY_DELAY_UNITS_ZH)}后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryReason: { network: '网络中断', provider_unavailable: '模型服务暂时不可用', rate_limit: '触发模型速率限制', timeout: '请求超时', unknown: '模型请求失败' }, safeResumePending: '正在验证…', safeResume: '安全恢复', thinking: '深度思考', truncated: '已截断', copied: '已复制', copying: '复制中', copyFailed: '复制失败', copy: '复制', editMessage: '编辑并重发', editMessageDisabledRunning: '当前回答仍在进行中,结束后再编辑', editMessageDisabledAttachments: '包含附件的历史消息暂不支持编辑并重发', editMessageDisabledQuotes: '包含引用的历史消息暂不支持编辑并重发', editMessageDisabledTransformedText: '通过显式技能发送的历史消息暂不支持编辑并重发', | ||
| userAriaLabel: '你发送的消息', systemAriaLabel: '系统消息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: (context) => `回答操作${context ? `:${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action}回答${context ? `:${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action}消息${context ? `:${context}` : ''}`, sourceAriaLabel: '本轮回答的来源', derivativesAriaLabel: '本轮回答的衍生', scheduledTaskTriggered: '定时任务触发', scheduledTaskTitle: (id) => `由定时任务触发 · ${id}`, legacyAutomationTriggered: '旧版自动化(仅历史)', legacyAutomationTitle: (id) => `由旧版自动化触发 · ${id} · 仅保留历史,不会再次执行`, goalContinued: 'Goal 自动继续', goalTitle: (id) => `由 Goal 继续执行 · ${id}`, agentGraphTriggered: 'Agent Graph 自动继续', agentGraphTitle: (graphId) => `由 Agent Graph 调度器触发 · ${graphId}`, | ||
| thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中断)', abortedByStop: '(已中断 · 由停止按钮触发)', | ||
| }, | ||
|
|
@@ -600,7 +648,7 @@ const CONVERSATION_COPY = { | |
| chooseAriaLabel: (label, branch) => branch ? `Choose project: ${label}, current branch ${branch}` : `Choose project: ${label}`, | ||
| }, | ||
| messages: { | ||
| you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${seconds}s (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryReason: { network: 'Network interrupted', provider_unavailable: 'Model service temporarily unavailable', rate_limit: 'Model rate limit reached', timeout: 'Request timed out', unknown: 'Model request failed' }, safeResumePending: 'Checking…', safeResume: 'Safe recovery', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages sent with an explicit skill', | ||
| you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatProviderRetryDelay(seconds, PROVIDER_RETRY_DELAY_UNITS_EN)} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryReason: { network: 'Network interrupted', provider_unavailable: 'Model service temporarily unavailable', rate_limit: 'Model rate limit reached', timeout: 'Request timed out', unknown: 'Model request failed' }, safeResumePending: 'Checking…', safeResume: 'Safe recovery', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages sent with an explicit skill', | ||
| userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: (context) => `Response actions${context ? `: ${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action} response${context ? `: ${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action} message${context ? `: ${context}` : ''}`, sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this one', scheduledTaskTriggered: 'Triggered by scheduled task', scheduledTaskTitle: (id) => `Triggered by scheduled task · ${id}`, legacyAutomationTriggered: 'Legacy Automation (history only)', legacyAutomationTitle: (id) => `Triggered by legacy Automation · ${id} · Historical only; it will not run again`, goalContinued: 'Continued by Goal', goalTitle: (id) => `Continued by Goal · ${id}`, agentGraphTriggered: 'Continued by Agent Graph', agentGraphTitle: (graphId) => `Triggered by the Agent Graph scheduler · ${graphId}`, | ||
| thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: '(Interrupted)', abortedByStop: '(Interrupted · Stop button)', | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P3]The description says short delays “keep the familiar seconds-only form (45秒后重试)”, but the previouszhcopy was${seconds} 秒后重试— with a space before秒. This assertion pins'45秒后重试(2/10)', so the short-delayzhstring does change, just subtly.I think dropping the space is the better copy and I am not asking you to restore it. Only flagging that the description says this path is unchanged when it is not, so a reader diffing screenshots is not left confused.
Separately, and to the test's credit: pinning both locales across second/minute/hour/day scales is exactly the right shape here — it fails if the ladder regresses, rather than restating the implementation.