dsh-better-tips 0.12.1 → 0.13.0

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/client.js CHANGED
@@ -10,32 +10,36 @@ window.__ModuleLoader__.load({
10
10
  const zh = {
11
11
  'ui.authProtected': '认证错误正文已隐藏,避免回显凭据。请核对密钥、接口地址和模型权限。',
12
12
  'ui.authBodyProtected': '认证响应正文已受保护;仅保留安全的状态与请求标识。',
13
- 'ui.details': '诊断详情', 'ui.bottom': '底层', 'ui.unconfirmed': '关联未确认',
13
+ 'ui.details': '底层原始详情', 'ui.bottom': '底层', 'ui.unconfirmed': '关联未确认',
14
14
  'ui.candidates': '候选捕获 · {count}', 'ui.loading': '加载中',
15
15
  'ui.empty': '未取得额外网络诊断', 'ui.disabled': '额外网络诊断已关闭', 'ui.captureUnavailable': '额外网络诊断不可用', 'ui.loadFailed': '详情读取失败 · {message}',
16
16
  'ui.refresh': '重新读取', 'ui.bodyPending': '响应正文读取中', 'ui.bodyEmpty': '响应正文为空',
17
17
  'ui.bodyTruncated': '响应正文已截断', 'ui.bodyUnreadable': '响应正文读取失败',
18
- 'ui.copyError': '复制完整错误', 'ui.copyCoordinate': '复制日志坐标', 'ui.copyPath': '复制路径',
18
+ 'ui.copyCapture': '复制截获错误', 'ui.copyError': '复制完整错误', 'ui.copyCoordinate': '复制日志坐标', 'ui.copyPath': '复制路径',
19
19
  'ui.copied': '已复制', 'ui.copyFailed': '复制失败,请手动选择文本', 'ui.copying': '复制中',
20
20
  'ui.error': '失败', 'ui.stopped': '已停止', 'ui.retry': '重试',
21
21
  'ui.retried': '已重试', 'ui.cancelled': '重试取消', 'ui.scheduled': '等待重试',
22
22
  'compact.title': '上下文已压缩', 'compact.completed': '{items} 条 · ~{tokens} tokens',
23
23
  'compact.running': '压缩中', 'compact.done': '压缩完成',
24
24
  'compact.failed': '压缩失败', 'compact.failedAuto': '自动压缩失败',
25
+ 'compact.recordMissing': '本次压缩的具体失败记录尚未加载,当前仅有命令提示。', 'compact.recordFailed': '本次压缩失败记录读取失败。',
26
+ 'compact.errorProtected': '压缩失败;持久化诊断缺少错误类型,正文已隐藏以避免回显凭据。',
25
27
  'compact.attempt': '尝试 {index}',
26
28
  'ui.expand': '错误详情', 'ui.original': 'DSH 原生报错', 'ui.network': '插件捕获的底层报错',
27
29
  'ui.nativeExcerpt': 'DSH 原生报错 · 摘要', 'ui.overviewSource': '插件整理 · 重试概览', 'ui.open': '展开', 'ui.close': '收起',
30
+ 'ui.earlierErrors': '更早的报错 · {count}', 'ui.earlierBlock': '更早的报错',
28
31
  'ui.history': '重试记录 · {count}', 'ui.record': '失败记录 {index}',
29
- 'ui.preview': '摘要 · 完整原文见详情', 'ui.categoryOverview': '类别概览', 'ui.different': '{count} 类错误',
32
+ 'ui.categoryOverview': '类别概览', 'ui.different': '{count} 类错误',
30
33
  'ui.attempts': '请求尝试 · {count}', 'ui.logSection': '日志定位',
31
34
  'ui.timingMatch': '按时间关联 · 未确认请求身份', 'ui.wait': '计划等待 {ms} ms',
32
35
  'ui.historyNote': '按发生顺序;同一重试的等待与开始状态仅记一次。',
33
- 'report.copy': '复制排障报告', 'report.building': '生成中', 'report.partial': '已复制 · 部分信息缺失',
34
- 'report.fallback': '复制失败,可选择下方报告文本', 'report.preview': '查看报告文本',
36
+ 'report.copy': '一键复制排障报告', 'report.building': '生成中', 'report.partial': '已复制 · 部分信息缺失',
37
+ 'report.fallback': '复制失败,可选择下方报告文本',
35
38
  'report.notice': '分享前检查:报错或响应可能回显输入;仅过滤常见凭据,不保证清除个人信息。',
36
39
  'compact.usage': '摘要请求用量(非会话总量)', 'log.contextWarning': '相邻事件可能含会话内容,分享前请检查。',
37
40
  'report.responseState': '响应状态',
38
41
  'report.intro': '记录于{time}:使用“{provider}”渠道的“{model}”模型时遇到{error}。',
42
+ 'report.plugins': '已启用第三方插件', 'report.pluginsPartial': '部分插件信息未能确认', 'report.pluginsUnavailable': '清单读取失败或不可用', 'report.pluginsNone': '无',
39
43
  'report.missing': '未记录', 'report.version': '截止生成本报告时', 'report.native': 'DSH 原生报错', 'report.underlying': '插件捕获的底层报错',
40
44
  'report.noEvidence': '未取得唯一匹配的底层证据', 'report.ambiguous': '有多个候选,未混入报告', 'report.readFailed': '读取失败',
41
45
  'report.unconfirmed': '同一时间附近捕获到以下报错:', 'report.summaryError': '摘要请求报错',
@@ -47,32 +51,36 @@ window.__ModuleLoader__.load({
47
51
  const en = {
48
52
  'ui.authProtected': 'Authentication error text is hidden to avoid echoing credentials. Check the key, endpoint and model permissions.',
49
53
  'ui.authBodyProtected': 'Authentication response text is protected; only safe status and request identifiers are retained.',
50
- 'ui.details': 'Diagnostic details', 'ui.bottom': 'Underlying', 'ui.unconfirmed': 'Unconfirmed match',
54
+ 'ui.details': 'Underlying raw details', 'ui.bottom': 'Underlying', 'ui.unconfirmed': 'Unconfirmed match',
51
55
  'ui.candidates': 'Candidate captures · {count}', 'ui.loading': 'Loading',
52
56
  'ui.empty': 'No additional network diagnosis', 'ui.disabled': 'Additional network diagnosis is disabled', 'ui.captureUnavailable': 'Additional network diagnosis unavailable', 'ui.loadFailed': 'Details unavailable · {message}',
53
57
  'ui.refresh': 'Read again', 'ui.bodyPending': 'Reading response body', 'ui.bodyEmpty': 'Response body is empty',
54
58
  'ui.bodyTruncated': 'Response body truncated', 'ui.bodyUnreadable': 'Response body read failed',
55
- 'ui.copyError': 'Copy full error', 'ui.copyCoordinate': 'Copy log coordinates', 'ui.copyPath': 'Copy path',
59
+ 'ui.copyCapture': 'Copy captured error', 'ui.copyError': 'Copy full error', 'ui.copyCoordinate': 'Copy log coordinates', 'ui.copyPath': 'Copy path',
56
60
  'ui.copied': 'Copied', 'ui.copyFailed': 'Copy failed; select the text manually', 'ui.copying': 'Copying',
57
61
  'ui.error': 'Failed', 'ui.stopped': 'Stopped', 'ui.retry': 'Retrying',
58
62
  'ui.retried': 'Retried', 'ui.cancelled': 'Retry cancelled', 'ui.scheduled': 'Retry scheduled',
59
63
  'compact.title': 'Context compacted', 'compact.completed': '{items} items · ~{tokens} tokens',
60
64
  'compact.running': 'Compacting', 'compact.done': 'Compaction complete',
61
65
  'compact.failed': 'Compaction failed', 'compact.failedAuto': 'Automatic compaction failed',
66
+ 'compact.recordMissing': 'The specific failure record for this compaction is not loaded; only the command notice is available.', 'compact.recordFailed': 'Could not read this compaction failure record.',
67
+ 'compact.errorProtected': 'Compaction failed; durable diagnosis has no error type, so its text is withheld to avoid credential echoes.',
62
68
  'compact.attempt': 'Attempt {index}',
63
69
  'ui.expand': 'Error details', 'ui.original': 'DSH native error', 'ui.network': 'Plugin-captured underlying error',
64
70
  'ui.nativeExcerpt': 'DSH native error · excerpt', 'ui.overviewSource': 'Plugin summary · retries', 'ui.open': 'Expand', 'ui.close': 'Collapse',
71
+ 'ui.earlierErrors': 'Earlier errors · {count}', 'ui.earlierBlock': 'Earlier error',
65
72
  'ui.history': 'Retry history · {count}', 'ui.record': 'Failure record {index}',
66
- 'ui.preview': 'Excerpt · full original in details', 'ui.categoryOverview': 'Category overview', 'ui.different': '{count} error categories',
73
+ 'ui.categoryOverview': 'Category overview', 'ui.different': '{count} error categories',
67
74
  'ui.attempts': 'Request attempts · {count}', 'ui.logSection': 'Log location',
68
75
  'ui.timingMatch': 'Time correlation · request identity unconfirmed', 'ui.wait': 'planned wait {ms} ms',
69
76
  'ui.historyNote': 'Chronological; scheduled and started states of one retry count once.',
70
77
  'report.copy': 'Copy troubleshooting report', 'report.building': 'Preparing', 'report.partial': 'Copied · some information missing',
71
- 'report.fallback': 'Copy failed; select the report below', 'report.preview': 'View report text',
78
+ 'report.fallback': 'Copy failed; select the report below',
72
79
  'report.notice': 'Review before sharing: errors/responses may echo input. Common credential filtering is not personal-data removal.',
73
80
  'compact.usage': 'Summary request usage (not session total)', 'log.contextWarning': 'Nearby events may contain conversation content. Review before sharing.',
74
81
  'report.responseState': 'Response state',
75
82
  'report.intro': 'At {time}, model “{model}” via provider “{provider}” returned {error}.',
83
+ 'report.plugins': 'Enabled third-party plugins', 'report.pluginsPartial': 'Some plugin information could not be verified', 'report.pluginsUnavailable': 'Inventory unavailable or read failed', 'report.pluginsNone': 'none',
76
84
  'report.missing': 'not recorded', 'report.version': 'As of this report', 'report.native': 'DSH native error', 'report.underlying': 'Plugin-captured underlying error',
77
85
  'report.noEvidence': 'No uniquely matched underlying evidence', 'report.ambiguous': 'Multiple candidates omitted', 'report.readFailed': 'Read failed',
78
86
  'report.unconfirmed': 'The following was captured around the same time:', 'report.summaryError': 'Summary request error',
@@ -193,13 +201,13 @@ window.__ModuleLoader__.load({
193
201
  '.bt_error[open]>summary:before,.bt_compact[open]>summary:before{content:"⌄"}',
194
202
  '.bt_title{flex:none;color:var(--dsw-alias-label-secondary)}',
195
203
  '.bt_error .bt_primary{color:var(--dsw-alias-state-error-primary)}',
196
- '.bt_primary{font:inherit;font-family:ui-monospace,monospace;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}',
204
+ '.bt_primary{color:var(--dsw-alias-state-error-primary);font:inherit;font-family:ui-monospace,monospace;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}',
197
205
  '.bt_logOpen{flex:1 1 100%;min-width:0}',
198
206
  '.bt_actions{display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;min-width:0}',
199
- '.bt_action,.bt_logButton{border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;min-height:28px;padding:3px 6px;cursor:pointer;text-decoration:none}',
200
- '.bt_action:hover,.bt_logButton:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}',
207
+ '.bt_action,.bt_logButton{display:inline-flex;align-items:center;justify-content:center;gap:4px;border:1px solid var(--dsw-alias-border-l1);border-radius:3px;background:transparent;color:var(--dsw-alias-label-primary);font:400 12px/1.4 system-ui,sans-serif;min-height:24px;padding:2px 7px;cursor:pointer;text-decoration:none;width:fit-content}',
208
+ '.bt_action:hover,.bt_logButton:hover{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}',
201
209
  '.bt_action:disabled,.bt_logButton:disabled{cursor:not-allowed;opacity:.55;text-decoration:none}',
202
- '.bt_action:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
210
+ '.bt_action:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
203
211
  '.bt_flag{flex:none;font-size:11px;color:var(--dsw-alias-label-secondary)}',
204
212
  '.bt_body{margin:4px 0 6px 14px;display:grid;gap:6px;min-width:0}',
205
213
  '.bt_body>.bt_refresh{justify-self:start}',
@@ -208,27 +216,33 @@ window.__ModuleLoader__.load({
208
216
  'border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-sm);user-select:text}',
209
217
  '.bt_meta{color:var(--dsw-alias-label-secondary);font-size:12px;overflow-wrap:anywhere}',
210
218
  '.bt_capture{display:grid;gap:3px;min-width:0}',
211
- '.bt_advice,.bt_captureAdvice{display:flex;align-items:baseline;gap:8px;color:var(--dsw-alias-label-secondary);font-size:12px;overflow-wrap:anywhere}',
212
- '.bt_logButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
219
+ '.bt_advice,.bt_captureAdvice{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;padding:2px 0;color:var(--dsw-alias-label-primary);font-size:12px;overflow-wrap:anywhere}.bt_advice>.bt_title,.bt_captureAdvice>.bt_title{font-weight:600;color:var(--dsw-alias-state-error-primary)}',
220
+ '.bt_logButton:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
213
221
  '.bt_logPanel{margin:6px 0;padding:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;min-width:0}',
214
222
  '.bt_logTarget{border-left:3px solid var(--dsw-alias-state-error-primary);padding-left:8px;min-width:0}',
215
223
  '.bt_logHeading{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}',
216
224
  '.bt_advice{grid-column:1/-1;margin:1px 0 2px;min-width:0}',
217
225
  '.bt_compact{color:var(--dsw-alias-label-secondary)}',
218
- '.bt_error>summary:focus-visible,.bt_compact>summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
226
+ '.bt_error>summary:focus-visible,.bt_compact>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
219
227
  '.bt_detailBody{margin:4px 0 8px 10px;gap:10px}.bt_section{min-width:0;border-top:1px solid var(--dsw-alias-border-l1);padding-top:7px}.bt_heading{margin:0 0 4px;font:inherit;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
220
- '.bt_section>summary,.bt_attempt>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_section>summary:focus-visible,.bt_attempt>summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.bt_attempt{min-width:0;margin:4px 0;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l1)}.bt_attempt>div{display:grid;gap:5px;padding:6px 0}.bt_attemptPreview{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}',
221
- '.bt_attempt>summary,.bt_network>summary{list-style:none}.bt_attempt>summary::-webkit-details-marker,.bt_network>summary::-webkit-details-marker{display:none}.bt_attempt>summary:before,.bt_network>summary:before{content:"›";flex:none;color:var(--dsw-alias-label-secondary)}.bt_attempt[open]>summary:before,.bt_network[open]>summary:before{content:"⌄"}.bt_attemptPreview{font:inherit;font-family:ui-monospace,monospace}.bt_original{max-height:340px;overflow:auto}.bt_network>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_network>summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
222
- '.bt_reportText>summary{cursor:pointer;min-height:28px;font-size:12px;color:var(--dsw-alias-label-secondary)}.bt_reportText>summary:focus-visible,.bt_reportText pre:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
228
+ '.bt_section>summary,.bt_attempt>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_section>summary:focus-visible,.bt_attempt>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}.bt_attempt{min-width:0;margin:4px 0;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l1)}.bt_attempt>div{display:grid;gap:5px;padding:6px 0}.bt_attemptPreview{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}',
229
+ '.bt_attempt>summary,.bt_network>summary{list-style:none}.bt_attempt>summary::-webkit-details-marker,.bt_network>summary::-webkit-details-marker{display:none}.bt_attempt>summary:before,.bt_network>summary:before{content:"›";flex:none;color:var(--dsw-alias-label-secondary)}.bt_attempt[open]>summary:before,.bt_network[open]>summary:before{content:"⌄"}.bt_attemptPreview{font:inherit;font-family:ui-monospace,monospace}.bt_original{max-height:340px;overflow:auto}.bt_network>summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;min-height:28px;cursor:pointer}.bt_network>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
230
+ '.bt_reportText>summary{cursor:pointer;min-height:28px;font-size:12px;color:var(--dsw-alias-label-secondary)}.bt_reportText>summary:focus-visible,.bt_reportText pre:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
223
231
  '.bt_source{font:600 12px/1.6 system-ui,sans-serif;color:var(--dsw-alias-label-primary)}',
224
232
  '.bt_errorGroup details{min-width:0}.bt_errorGroup details>summary,.bt_compact>summary{display:flex;align-items:center;flex-wrap:wrap;gap:5px 8px;min-height:32px;padding:4px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;list-style:none}',
225
233
  '.bt_errorGroup details>summary::-webkit-details-marker{display:none}.bt_errorGroup details>summary:before,.bt_compact>summary:before{content:none}.bt_errorGroup details[open]>summary:before,.bt_compact[open]>summary:before{content:none}',
226
234
  '.bt_toggleLabel{display:inline-flex;flex:none;padding:1px 5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);font:600 12px/1.7 system-ui,sans-serif;color:var(--dsw-alias-label-primary)}.bt_toggleLabel>.bt_whenOpen{display:none}details[open]>summary>.bt_toggleLabel>.bt_whenOpen{display:inline}details[open]>summary>.bt_toggleLabel>.bt_whenClosed{display:none}',
227
- '.bt_errorGroup details>summary:hover,.bt_compact>summary:hover{border-color:var(--dsw-alias-state-error-primary)}.bt_errorGroup details>summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
235
+ '.bt_errorGroup details>summary:hover,.bt_compact>summary:hover{border-color:var(--dsw-alias-state-error-primary)}.bt_errorGroup details>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
228
236
  '.bt_detailBody{margin:0;padding:0}.bt_section{border-top:0;padding-top:4px}.bt_attempt{margin:8px 0;padding:0;border-left:0}',
229
237
  '.bt_depth{display:grid;gap:8px;min-width:0;margin:8px 0 8px 10px;padding:4px 0 4px 12px;border-left:2px solid var(--dsw-alias-state-error-primary)}.bt_errorGroup .bt_depth>.bt_body{margin:0;padding:0}.bt_attempt>.bt_depth{padding:4px 0 4px 12px}.bt_logPanel{margin-left:10px;border-left:2px solid var(--dsw-alias-state-error-primary)}',
230
238
  '.bt_heading{margin-bottom:5px}.bt_original{font-size:12px}.bt_toggleLabel[hidden]{display:none}',
231
- '@media(max-width:640px){.bt_errorGroup{grid-template-columns:minmax(0,1fr);padding-left:9px}.bt_action,.bt_logButton{min-height:32px}}',
239
+ '.bt_earlierNode{border-left:0;padding-left:0}.bt_earlierNode>.bt_depth,.bt_earlierErrors>.bt_depth,.bt_earlierError>.bt_depth{border-left:0;margin-left:0;padding-left:0}',
240
+ '.bt_earlierNode>summary{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;font-size:12px;color:var(--dsw-alias-label-secondary)}.bt_earlierNode>summary::-webkit-details-marker{display:none}.bt_earlierNode>summary:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary);outline-offset:2px}',
241
+ '.bt_failureTitle{font-size:17px;font-weight:700;line-height:1.5;color:var(--dsw-alias-state-error-primary)}',
242
+ '.bt_detailPair{display:grid;grid-template-columns:minmax(0,1fr);align-items:start;gap:8px;min-width:0}.bt_detailPair>details{margin:0}.bt_detailPair .bt_depth{margin-left:0;padding-left:8px}',
243
+ '.bt_errorGroup>.bt_network>.bt_body{margin-left:0}.bt_primary{font-size:15px;line-height:1.6}.bt_original{font-size:14px}',
244
+ '.bt_history{padding-top:0;display:grid;gap:6px}.bt_history>.bt_attempt{margin:0}.bt_history .bt_attempt>.bt_network>.bt_body{margin-left:0}.bt_attempt>.bt_captureAdvice{display:flex;align-items:baseline;gap:4px 8px;padding:2px 0}',
245
+ '@media(max-width:640px){.bt_errorGroup{grid-template-columns:minmax(0,1fr);padding-left:9px}.bt_detailPair{grid-template-columns:minmax(0,1fr)}}',
232
246
  ].join('')
233
247
  const text = (value) => value === undefined || value === null ? '' : String(value)
234
248
  const rawFailure = (failure) => [failure?.code ? `[${failure.code}]` : '', text(failure?.message)].filter(Boolean).join(' ')
@@ -360,21 +374,51 @@ window.__ModuleLoader__.load({
360
374
  function RefreshButton ({ refresh, t }) {
361
375
  return refresh ? h('button', { type: 'button', className: 'bt_action bt_refresh', onClick: refresh }, t('ui.refresh')) : null
362
376
  }
363
- function captureBlock (item, index, t, unconfirmed = false, showFailure = false) {
377
+ function capturePreview (detail) {
378
+ const headline = nativeHeadline(detail)
379
+ let message = ''
380
+ if (detail?.kind === 'response' && !detail.protected && detail.bodyReason !== 'credential-protection') {
381
+ message = text(detail.body)
382
+ if (!detail.bodyState || detail.bodyState === 'complete') {
383
+ try {
384
+ const parsed = JSON.parse(message)
385
+ const providerMessage = parsed?.error?.message ?? parsed?.message
386
+ if (typeof providerMessage === 'string') message = providerMessage
387
+ } catch { /* Non-JSON responses stay literal. */ }
388
+ }
389
+ }
390
+ return failurePreview({ message: [headline, message].filter(Boolean).join(' · ') })
391
+ }
392
+ function captureDetails (item, t, unconfirmed = false) {
393
+ return item?.detail ? disclosureBlock({ className: 'bt_networkDetails' },
394
+ h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.details')),
395
+ unconfirmed ? h('div', { className: 'bt_meta' }, t('ui.unconfirmed')) : null,
396
+ unconfirmed ? adviceRow(detailCheck(item.detail), t, 'bt_captureAdvice') : null,
397
+ h('pre', { className: 'bt_raw' }, captureText(item))) : null
398
+ }
399
+ function captureBlock (item, index, t, unconfirmed = false, showFailure = false, details = true, inlineCopy = true, captureAdvice = true) {
400
+ const preview = capturePreview(item?.detail)
364
401
  return h('div', { className: 'bt_capture', key: `capture-${index}` },
365
402
  unconfirmed ? h('span', { className: 'bt_meta' }, t('ui.unconfirmed')) : null,
366
403
  showFailure && item?.failure ? h('span', { className: 'bt_meta' }, rawFailure(item.failure)) : null,
367
- h('pre', { className: 'bt_raw' }, captureText(item)),
368
- item?.detail?.kind === 'response' ? h('span', { className: 'bt_meta' },
404
+ preview.value ? h('code', { className: 'bt_primary' }, preview.value) : null,
405
+ inlineCopy ? h(CopyButton, { value: captureText(item), label: 'ui.copyCapture', t }) : null,
406
+ details ? captureDetails(item, t) : null,
407
+ item?.detail?.kind === 'response' && (item.detail.bodyReason === 'credential-protection' || item.detail.bodyState !== 'complete' && item.detail.bodyState !== undefined || !item.detail.body) ? h('span', { className: 'bt_meta' },
369
408
  t(item.detail.bodyReason === 'credential-protection' ? 'ui.authBodyProtected' : item.detail.bodyState === 'pending' ? 'ui.bodyPending' : item.detail.bodyState === 'truncated' ? 'ui.bodyTruncated'
370
409
  : item.detail.bodyState === 'unreadable' || item.detail.bodyState === 'error' ? 'ui.bodyUnreadable'
371
- : !item.detail.body ? 'ui.bodyEmpty' : 'ui.details')) : null,
372
- unconfirmed ? adviceRow(detailCheck(item?.detail), t, 'bt_captureAdvice') : null)
410
+ : 'ui.bodyEmpty')) : null,
411
+ unconfirmed && captureAdvice ? adviceRow(detailCheck(item?.detail), t, 'bt_captureAdvice') : null)
412
+ }
413
+ function networkSection (selection, state, refresh, t, details = true) {
414
+ return h('section', { className: 'bt_section bt_network' },
415
+ h('h4', { className: 'bt_heading' }, t('ui.network')),
416
+ evidenceBody({ state, ...selection, refresh, details }, t))
373
417
  }
374
- function evidenceBody ({ state, failure, chosen, unconfirmed, candidates = [], extra, refresh }, t) {
418
+ function evidenceBody ({ state, failure, chosen, unconfirmed, candidates = [], extra, refresh, details = true, inlineCopy = true, captureAdvice = true }, t) {
375
419
  return h('div', { className: 'bt_body' },
376
420
  rawFailure(failure) ? h('pre', { className: 'bt_raw' }, rawFailure(failure)) : null,
377
- chosen?.detail ? captureBlock(chosen, 0, t, unconfirmed) : null,
421
+ chosen?.detail ? captureBlock(chosen, 0, t, unconfirmed, false, details, inlineCopy, captureAdvice) : null,
378
422
  candidates.length ? disclosureBlock( null,
379
423
  h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.candidates', { count: candidates.length })),
380
424
  ...candidates.map((item, index) => disclosureBlock( { className: 'bt_attempt', key: item.attemptId ?? index },
@@ -458,8 +502,12 @@ window.__ModuleLoader__.load({
458
502
  .replace(/\b(?:api[_-]?key|token|secret)["']?\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, '[credential redacted]')
459
503
  if (Array.isArray(value)) return value.map(safeLogValue)
460
504
  if (!value || typeof value !== 'object') return value
505
+ const compactionError = value.type === 'compaction/end' && typeof value.data?.error === 'string'
506
+ ? safeCompactionFailure(value.data.error) : undefined
461
507
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key,
462
- value.code === 'AUTH' && (key === 'message' || key === 'body') ? '[AUTH diagnostic hidden]' : safeLogValue(item)]))
508
+ value.type === 'compaction/end' && key === 'data' && typeof item?.error === 'string'
509
+ ? safeLogValue({ ...item, error: compactionError?.message || '[compaction diagnostic hidden]' })
510
+ : value.code === 'AUTH' && (key === 'message' || key === 'body') ? '[AUTH diagnostic hidden]' : safeLogValue(item)]))
463
511
  }
464
512
  const reportTime = (value, t) => {
465
513
  if (!Number.isFinite(value) || Math.abs(value) > 8640000000000000) return t('report.missing')
@@ -493,11 +541,16 @@ window.__ModuleLoader__.load({
493
541
  provider: row.provider || record?.provider || selected?.context?.provider || missing,
494
542
  model: record?.model || selected?.context?.model || missing,
495
543
  error: reportExcerpt(headline, 100, t).replace(/[.。…]+$/, ''),
496
- }), t('report.version') + ': DSH ' + (runtime?.dshVersion || missing) + ' · Better Tips ' + (runtime?.pluginVersion || missing),
497
- '', t('report.native') + ': ' + reportExcerpt(native, 600, t)]
544
+ }), t('report.version') + ': DSH ' + (runtime?.dshVersion || missing) + ' · Better Tips ' + (runtime?.pluginVersion || missing)]
545
+ const plugins = Array.isArray(runtime?.enabledPlugins) ? runtime.enabledPlugins : []
546
+ const pluginNames = [...new Set(plugins.filter((plugin) => typeof plugin?.name === 'string' && plugin.name.trim())
547
+ .map((plugin) => plugin.name + '@' + (typeof plugin.version === 'string' && plugin.version.trim() ? plugin.version : missing)))].sort()
548
+ const inventory = runtime?.pluginsStatus === 'complete' ? pluginNames.join('、') || t('report.pluginsNone')
549
+ : (pluginNames.length ? pluginNames.join('、') + ' · ' : '') + t(runtime?.pluginsStatus === 'partial' ? 'report.pluginsPartial' : 'report.pluginsUnavailable')
550
+ lines.push(t('report.plugins') + ': ' + inventory, '', t('report.native') + ': ' + reportExcerpt(native, 600, t))
498
551
  if (record || selection.unconfirmed) lines.push('', t('report.unconfirmed'))
499
552
  if (record) lines.push(t('report.summaryError') + ': ' +
500
- (protectedAuth ? t('report.protected') : reportExcerpt(rawFailure(selected?.failure ?? selected?.thrown) || missing, 300, t)))
553
+ (protectedAuth ? t('report.protected') : reportExcerpt(rawFailure(selected?.failure ?? selected?.thrown) || (selected?.finishReason ? 'finish=' + selected.finishReason : missing), 300, t)))
501
554
  if (!detail) lines.push(t('report.underlying') + ': ' +
502
555
  (error ? t('report.noEvidence') + ' · ' + t('report.readFailed') + ': ' + reportExcerpt(error, 80, t)
503
556
  : capture?.installed === false ? t(capture.reason === 'config-disabled' ? 'ui.disabled' : 'ui.captureUnavailable')
@@ -552,11 +605,18 @@ window.__ModuleLoader__.load({
552
605
  catch (error) { problem = controller.signal.aborted ? 'Request timeout' : text(error?.message ?? error) }
553
606
  }
554
607
  if (!alive.current || operation.current !== controller) return
608
+ let environment
609
+ try { environment = await fetchJson('api/better-tips/environment', controller.signal) }
610
+ catch { environment = undefined }
611
+ if (!alive.current || operation.current !== controller) return
555
612
  const selected = select?.({ data }) ?? {}
556
- const value = troubleshootingReport({ failure, row, title, runtime: data?.runtime,
613
+ const runtime = { ...data?.runtime, ...environment?.runtime,
614
+ enabledPlugins: environment?.runtime?.enabledPlugins,
615
+ pluginsStatus: Array.isArray(environment?.runtime?.enabledPlugins) ? environment.runtime.pluginsStatus ?? 'unavailable' : 'unavailable' }
616
+ const value = troubleshootingReport({ failure, row, title, runtime,
557
617
  selection: selected, record: selected.record ?? record, error: problem, capture: data?.capture }, t)
558
618
  setReport(value)
559
- const partial = !data?.runtime?.dshVersion || !selected.chosen?.detail || !!problem
619
+ const partial = !runtime?.dshVersion || runtime.pluginsStatus !== 'complete' || !selected.chosen?.detail || !!problem
560
620
  try {
561
621
  if (typeof navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable')
562
622
  await navigator.clipboard.writeText(value)
@@ -568,9 +628,7 @@ window.__ModuleLoader__.load({
568
628
  h('div', { className: 'bt_actions' },
569
629
  h('button', { type: 'button', className: 'bt_action bt_reportCopy', title: t('report.notice'), disabled: status === 'report.building', onClick: copy }, t('report.copy')),
570
630
  status ? h('span', { className: 'bt_meta', role: 'status' }, t(status)) : null),
571
- report ? disclosureBlock( { className: 'bt_reportText', open: status === 'report.fallback' },
572
- h('summary', { className: 'bt_meta' }, toggleLabel(t), t('report.preview')),
573
- h('div', { className: 'bt_meta' }, t('report.notice')),
631
+ report && status === 'report.fallback' ? h('div', { className: 'bt_reportText' },
574
632
  h('pre', { className: 'bt_raw', tabIndex: 0 }, report)) : null)
575
633
  }
576
634
  function RawLog ({ sessionId, node, t, capture, unconfirmed }) {
@@ -661,40 +719,40 @@ window.__ModuleLoader__.load({
661
719
  const shortened = excerpt !== original || points.length > 120 || first.length < original.length
662
720
  return { value: points.slice(0, 120).join('') + (points.length > 120 || first.length < excerpt.length ? '…' : ''), shortened }
663
721
  }
722
+ function originalControls (failure, t) {
723
+ return h('div', { className: 'bt_originalControls' },
724
+ h('pre', { className: 'bt_original' }, rawFailure(failure)),
725
+ h(CopyButton, { value: rawFailure(failure), label: 'ui.copyError', t }))
726
+ }
664
727
  function errorRow (title, failure, state, selection, t, log, disclosure = {}) {
665
- const original = rawFailure(failure)
666
- const preview = failurePreview(failure)
667
- const bottom = nativeHeadline(selection.chosen?.detail)
668
728
  const check = selection.check ?? (text(failure?.message) === 'Stream ended without finish_reason' ? 'finishReason'
669
729
  : selection.chosen?.detail && !selection.unconfirmed ? detailCheck(selection.chosen.detail) : failureCheck(failure))
730
+ const nativeFallback = disclosure.fallbackNative && !selection.chosen?.detail
670
731
  return h('div', { className: 'bt_errorGroup' },
671
- h('div', { className: 'bt_originalRow' },
672
- h('span', { className: 'bt_title' }, title),
673
- h('span', { className: 'bt_source' }, t(disclosure.overview ? 'ui.overviewSource' : 'ui.nativeExcerpt')),
674
- h('code', { className: 'bt_preview' }, preview.value),
675
- preview.shortened ? h('span', { className: 'bt_meta' }, t('ui.preview')) : null),
676
- failure?.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_protected' }, t('ui.authProtected')) : adviceRow(check, t),
677
- disclosure.report ?? null,
678
- disclosureBlock( { className: 'bt_error bt_disclosure', open: disclosure.open, onToggle: disclosure.onToggle },
679
- h('summary', null, toggleLabel(t), h('span', { className: 'bt_title' }, t('ui.expand')), disclosure.countLabel || null),
680
- h('div', { className: 'bt_body bt_detailBody' },
681
- disclosure.historyOnly ? null : h('section', { className: 'bt_section' },
682
- h('h4', { className: 'bt_heading' }, t('ui.original')),
683
- h('pre', { className: 'bt_original' }, original),
684
- h(CopyButton, { value: original, label: 'ui.copyError', t })),
685
- disclosure.history ?? null,
686
- disclosure.historyOnly || disclosure.network === false ? null : disclosureBlock( { className: 'bt_section bt_network' },
687
- h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.network'),
688
- bottom ? h('code', { className: 'bt_primary' }, bottom) : null,
689
- selection.unconfirmed ? h('span', { className: 'bt_flag' }, t('ui.unconfirmed')) : null),
690
- evidenceBody({ state, ...selection, refresh: log?.refresh }, t)),
691
- log && !disclosure.historyOnly ? h('section', { className: 'bt_section' },
692
- h('h4', { className: 'bt_heading' }, t('ui.logSection')),
693
- h(RawLog, { ...log, key: JSON.stringify([log.sessionId, logCoordinate(log.node)]), t, capture: selection.chosen, unconfirmed: selection.unconfirmed })) : null)))
732
+ h('div', { className: 'bt_failureTitle' }, title),
733
+ nativeFallback ? h('section', { className: 'bt_section bt_nativeFallback' },
734
+ h('h4', { className: 'bt_heading' }, t('ui.original')),
735
+ originalControls(failure, t),
736
+ evidenceBody({ state }, t))
737
+ : disclosure.historyOnly ? null : networkSection(selection, state, null, t, false),
738
+ disclosure.nativeStatus ?? null,
739
+ disclosure.historyOnly ? null : failure?.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_protected' }, t('ui.authProtected')) : adviceRow(check, t),
740
+ disclosure.historyOnly ? null : disclosure.report ?? null,
741
+ disclosure.historyOnly ? disclosure.history : h('div', { className: 'bt_detailPair' },
742
+ captureDetails(selection.chosen, t),
743
+ disclosureBlock({ className: 'bt_error bt_disclosure', open: disclosure.open, onToggle: disclosure.onToggle },
744
+ h('summary', null, toggleLabel(t), h('span', { className: 'bt_title' }, t(nativeFallback ? 'ui.logSection' : 'ui.original')), disclosure.countLabel || null),
745
+ h('div', { className: 'bt_body bt_detailBody' },
746
+ nativeFallback ? null : originalControls(failure, t),
747
+ disclosure.history ?? null,
748
+ log ? h('section', { className: 'bt_section' },
749
+ h('h4', { className: 'bt_heading' }, t('ui.logSection')),
750
+ h(RawLog, { ...log, key: JSON.stringify([log.sessionId, logCoordinate(log.node)]), t, capture: selection.chosen, unconfirmed: selection.unconfirmed })) : null))),
751
+ !disclosure.historyOnly ? h(RefreshButton, { refresh: log?.refresh, t }) : null)
694
752
  }
695
753
  function ErrorCard ({ title, failure, sessionId, node, url, revision, select, history, countLabel, historyOnly, reportFailure, reportRow, t }) {
696
754
  const [open, setOpen] = React.useState(false)
697
- const [state, refresh] = useEvidence(open ? url : '', revision)
755
+ const [state, refresh] = useEvidence(url, revision)
698
756
  const selection = select(state)
699
757
  return errorRow(title, failure, state, selection, t, { sessionId, node, refresh }, {
700
758
  open, onToggle: (event) => { if (!event.target || event.target === event.currentTarget) setOpen(event.currentTarget.open) },
@@ -719,43 +777,68 @@ window.__ModuleLoader__.load({
719
777
  const delay = Number.isFinite(current.delayMs) ? ' · ' + t('ui.wait', { ms: Math.round(current.delayMs) }) : ''
720
778
  return t(label) + ' ' + current.retry + '/' + maximum + delay
721
779
  }
722
- function failureCategory (failure) {
723
- const status = Number.isInteger(failure?.status) && failure.status >= 400 && failure.status <= 599
724
- ? String(failure.status) : text(failure?.message).match(/^\s*(?:HTTP\s+)?([45]\d{2})(?=\s|:|$)/)?.[1]
725
- const code = text(failure?.code)
726
- return text(failure?.message) === 'Stream ended without finish_reason' ? (code || 'TRANSPORT') + ' / finish_reason'
727
- : [code, status ? 'HTTP ' + status : ''].filter(Boolean).join(' / ') || 'UNKNOWN'
780
+ function isErrorNode (node) {
781
+ return node?.visibility !== 'hidden' && (node?.kind === 'turn-error' || node?.kind === 'model-retry' || node?.kind === KIND
782
+ || node?.kind === 'manual-compaction' && node.data?.command?.outcome?.kind === 'error')
783
+ }
784
+ function followingError (snapshot, node) {
785
+ const order = snapshot?.order
786
+ const index = order?.indexOf(node?.key) ?? -1
787
+ if (index < 0 || !snapshot?.nodes?.get) return ''
788
+ let latest = ''
789
+ for (let at = index + 1; at < order.length; at++) {
790
+ const next = snapshot.nodes.get(order[at])
791
+ if (!next) return latest
792
+ if (isErrorNode(next)) latest = next.key
793
+ }
794
+ return latest
795
+ }
796
+ function ErrorSequenceRow ({ component, useChat, node, t, ...props }) {
797
+ const successor = typeof useChat === 'function' ? useChat((snapshot) => followingError(snapshot, node)) : ''
798
+ const identity = JSON.stringify([props.sessionId, node?.key, successor])
799
+ const [choice, setChoice] = React.useState(null)
800
+ const open = choice?.identity === identity && choice.open
801
+ const content = () => h(component, { ...props, node, t,
802
+ key: JSON.stringify([props.sessionId, node?.key, node?.id, logCoordinate(node), node?.data?.message, node?.data?.endedAt]) })
803
+ if (!successor || !isErrorNode(node)) return content()
804
+ return disclosureBlock({ className: 'bt_earlierNode bt_errorGroup', open,
805
+ onToggle: (event) => { if (!event.target || event.target === event.currentTarget) setChoice({ identity, open: event.currentTarget.open }) } },
806
+ h('summary', null, toggleLabel(t), t('ui.earlierBlock')),
807
+ open ? content() : null)
808
+ }
809
+ function earlierErrors (blocks, t) {
810
+ if (blocks.length < 2) return blocks
811
+ return [disclosureBlock({ className: 'bt_earlierErrors', key: 'earlier:' + blocks.at(-1).props.key },
812
+ h('summary', null, toggleLabel(t), t('ui.earlierErrors', { count: blocks.length - 1 })),
813
+ ...blocks.slice(0, -1).map((block, index) => disclosureBlock({ className: 'bt_earlierError', key: block.props.key },
814
+ h('summary', null, toggleLabel(t), t('ui.record', { index: index + 1 })), block))), blocks.at(-1)]
728
815
  }
729
816
  function retryHistory (attempts, state, sessionId, node, t, refresh) {
817
+ const blocks = attempts.map((attempt, index) => {
818
+ const failure = attempt.failure ?? {}
819
+ const selection = selectStep(state, attempt.step, failure, attempt, sessionId)
820
+ const attemptNode = { ...node, kind: 'model-retry', data: { current: attempt } }
821
+ const check = text(failure.message) === 'Stream ended without finish_reason' ? 'finishReason'
822
+ : selection.chosen?.detail && !selection.unconfirmed ? detailCheck(selection.chosen.detail) : failureCheck(failure)
823
+ return h('div', { className: 'bt_attempt', key: attempt.seq ?? attempt.attemptId ?? index },
824
+ index < attempts.length - 1 ? h('div', { className: 'bt_heading' }, retryTitle(attempt, t)) : null,
825
+ networkSection({ ...selection, inlineCopy: false, captureAdvice: false }, state, null, t, false),
826
+ failure.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_meta' }, t('ui.authProtected')) : adviceRow(check, t, index === attempts.length - 1 ? 'bt_captureAdvice bt_advice' : 'bt_captureAdvice'),
827
+ selection.chosen?.detail ? h(CopyButton, { value: captureText(selection.chosen), label: 'ui.copyCapture', t }) : null,
828
+ h(ReportButton, { key: JSON.stringify([sessionId, attempt.seq, attempt.attemptId]),
829
+ url: requestUrl(sessionId, attempt.turn, 'retry'), state,
830
+ select: (fresh) => selectStep(fresh, attempt.step, failure, attempt, sessionId),
831
+ failure, row: attempt, title: retryTitle(attempt, t), t }),
832
+ h('div', { className: 'bt_detailPair' },
833
+ captureDetails(selection.chosen, t, selection.unconfirmed),
834
+ disclosureBlock({ className: 'bt_attemptNative' },
835
+ h('summary', null, toggleLabel(t), t('ui.original'), validSeq(attempt.seq) ? ' · seq=' + attempt.seq : ''),
836
+ originalControls(failure, t),
837
+ h(RawLog, { key: JSON.stringify([sessionId, logCoordinate(attemptNode)]), sessionId, node: attemptNode, t, capture: selection.chosen, unconfirmed: selection.unconfirmed }))))
838
+ })
839
+ const grouped = earlierErrors(blocks, t)
730
840
  return h('section', { className: 'bt_section bt_history' },
731
- h('h4', { className: 'bt_heading' }, t('ui.history', { count: attempts.length })),
732
- h('div', { className: 'bt_meta' }, t('ui.historyNote')),
733
- h(RefreshButton, { refresh, t }),
734
- ...attempts.map((attempt, index) => {
735
- const failure = attempt.failure ?? {}
736
- const selection = selectStep(state, attempt.step, failure, attempt, sessionId)
737
- const attemptNode = { ...node, kind: 'model-retry', data: { current: attempt } }
738
- return disclosureBlock( { className: 'bt_attempt', key: attempt.seq ?? attempt.attemptId ?? index },
739
- h('summary', null, toggleLabel(t), h('span', { className: 'bt_title' }, t('ui.record', { index: index + 1 })),
740
- h('span', { className: 'bt_meta' }, retryTitle(attempt, t) + (validSeq(attempt.seq) ? ' · seq=' + attempt.seq : '')),
741
- h('span', { className: 'bt_source' }, t('ui.nativeExcerpt')),
742
- h('code', { className: 'bt_attemptPreview' }, failurePreview(failure).value)),
743
- h('div', null,
744
- h('h4', { className: 'bt_heading' }, t('ui.original')),
745
- h('pre', { className: 'bt_original' }, rawFailure(failure)),
746
- h(CopyButton, { value: rawFailure(failure), label: 'ui.copyError', t }),
747
- h(ReportButton, { key: JSON.stringify([sessionId, attempt.seq, attempt.attemptId]),
748
- url: requestUrl(sessionId, attempt.turn, 'retry'), state,
749
- select: (fresh) => selectStep(fresh, attempt.step, failure, attempt, sessionId),
750
- failure, row: attempt, title: retryTitle(attempt, t), t }),
751
- failure.code === 'AUTH' && !failure.message ? h('div', { className: 'bt_meta' }, t('ui.authProtected')) : adviceRow(text(failure.message) === 'Stream ended without finish_reason' ? 'finishReason' : selection.chosen?.detail && !selection.unconfirmed ? detailCheck(selection.chosen.detail) : failureCheck(failure), t, 'bt_captureAdvice'),
752
- disclosureBlock( { className: 'bt_network' },
753
- h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.network'),
754
- nativeHeadline(selection.chosen?.detail) ? h('code', { className: 'bt_primary' }, nativeHeadline(selection.chosen.detail)) : null,
755
- selection.unconfirmed ? h('span', { className: 'bt_flag' }, t('ui.unconfirmed')) : null),
756
- evidenceBody({ state, ...selection, refresh }, t)),
757
- h(RawLog, { key: JSON.stringify([sessionId, logCoordinate(attemptNode)]), sessionId, node: attemptNode, t, capture: selection.chosen, unconfirmed: selection.unconfirmed })))
758
- }))
841
+ ...grouped.toReversed(), h(RefreshButton, { refresh, t }))
759
842
  }
760
843
  function ModelRetryRow ({ node, sessionId, t = (key) => key }) {
761
844
  const current = node?.data?.current ?? {}
@@ -763,13 +846,10 @@ window.__ModuleLoader__.load({
763
846
  if (current.retry === undefined) return null
764
847
  // Native node.attempts is producer-correlated durable history, not the volatile capture cache.
765
848
  const attempts = node.data.attempts?.length ? node.data.attempts : [current]
766
- const categories = new Set(attempts.map((attempt) => failureCategory(attempt.failure)))
767
- const countLabel = t('ui.history', { count: attempts.length }) + (categories.size > 1 ? ' · ' + t('ui.different', { count: categories.size }) : '')
768
- const summaryFailure = categories.size > 1 ? { message: t('ui.categoryOverview') + ' · ' + [...categories].join(' · ') } : failure
769
- return h(ErrorCard, { title: retryTitle(current, t), failure: summaryFailure, reportFailure: failure, reportRow: current, sessionId, node, t, historyOnly: true,
849
+ return h(ErrorCard, { title: retryTitle(current, t), failure, sessionId, node, t, historyOnly: true,
770
850
  url: requestUrl(sessionId, current.turn, 'retry'), revision: JSON.stringify([node.id, attempts.length, current.seq, current.retry, current.retryState, failure]),
771
- select: (state) => ({ ...selectStep(state, current.step, failure, current, sessionId), ...(categories.size > 1 ? { check: 'mixed' } : {}) }),
772
- countLabel: () => countLabel, history: (state, refresh) => retryHistory(attempts, state, sessionId, node, t, refresh) })
851
+ select: (state) => selectStep(state, current.step, failure, current, sessionId),
852
+ history: (state, refresh) => retryHistory(attempts, state, sessionId, node, t, refresh) })
773
853
  }
774
854
  function compactionUrl (sessionId) {
775
855
  return sessionId ? `api/better-tips/compaction?${new URLSearchParams({ sessionId, source: 'compaction-row' })}` : ''
@@ -779,7 +859,7 @@ window.__ModuleLoader__.load({
779
859
  let best
780
860
  let distance = Infinity
781
861
  for (const record of state?.data?.records ?? []) {
782
- if (record.status !== status && !(status === 'error' && record.status === 'thrown')) continue
862
+ if (record.status !== status && !(status === 'error' && ['thrown', 'max-tokens', 'incomplete'].includes(record.status))) continue
783
863
  const start = record.startedAt
784
864
  const end = record.endedAt ?? start
785
865
  if (Number.isFinite(startAt) && start < startAt - 5000) continue
@@ -798,32 +878,95 @@ window.__ModuleLoader__.load({
798
878
  h('h4', { className: 'bt_heading' }, t('ui.attempts', { count: record.attempts?.length ?? 0 })),
799
879
  h('div', { className: 'bt_meta' }, t('ui.timingMatch')),
800
880
  h('div', { className: 'bt_meta' }, facts.join(' · ')),
801
- ...(record.attempts ?? []).map((attempt, index) => {
881
+ ...earlierErrors((record.attempts ?? []).map((attempt, index) => {
802
882
  const failure = attempt.failure ?? attempt.thrown
803
- return disclosureBlock( { className: 'bt_attempt', key: attempt.attemptId ?? record.id + ':' + (attempt.index ?? index) },
804
- h('summary', null, toggleLabel(t), h('span', { className: 'bt_title' }, t('compact.attempt', { index: attempt.index ?? index + 1 })),
805
- h('span', { className: 'bt_source' }, t('ui.nativeExcerpt')),
806
- h('code', { className: 'bt_attemptPreview' }, failure ? failurePreview(failure).value : '')),
807
- h('div', null,
808
- failure ? h('h4', { className: 'bt_heading' }, t('ui.original')) : null,
809
- failure ? h('pre', { className: 'bt_original' }, rawFailure(failure)) : null,
810
- failure ? h(CopyButton, { value: rawFailure(failure), label: 'ui.copyError', t }) : null,
811
- attempt.detail ? disclosureBlock( { className: 'bt_network' },
812
- h('summary', { className: 'bt_meta' }, toggleLabel(t), t('ui.network')),
813
- captureBlock(attempt, index, t, true)) : h('span', { className: 'bt_meta' }, t('ui.empty'))))
814
- }))
883
+ return h('div', { className: 'bt_attempt', key: attempt.attemptId ?? record.id + ':' + (attempt.index ?? index) },
884
+ h('h4', { className: 'bt_heading' }, t('compact.attempt', { index: attempt.index ?? index + 1 }) + (attempt.finishReason ? ' · finish=' + attempt.finishReason : '')),
885
+ attempt.detail ? networkSection({ chosen: attempt, unconfirmed: true }, { data: {} }, null, t, false) : h('span', { className: 'bt_meta' }, t('ui.empty')),
886
+ h('div', { className: 'bt_detailPair' },
887
+ captureDetails(attempt, t),
888
+ failure ? disclosureBlock({ className: 'bt_attemptNative' },
889
+ h('summary', null, toggleLabel(t), t('ui.original')),
890
+ originalControls(failure, t)) : null))
891
+ }), t))
892
+ }
893
+ function safeCompactionFailure (message) {
894
+ if (typeof message !== 'string') return undefined
895
+ // errorChain persists text, not the original error code. Unknown text may
896
+ // be an AUTH message; never recover it merely because the transaction matches.
897
+ if (/\bAUTH\b/.test(message)) return { code: 'AUTH', message: '' }
898
+ if (message === 'summarization truncated at the token cap (incomplete checkpoint)') return { code: 'MAX_TOKENS', message }
899
+ if (message === 'summarization produced no text summary content'
900
+ || message === 'compaction summary cannot contain image output'
901
+ || /^summary is not smaller than the shadowed content \(\d+ estimated framed tokens >= \d+\)$/.test(message)
902
+ || /^compaction: (?:session surface changed during summarization|selected surface changed before summarization began|the selected span changed during summarization|the selected span was rewritten during summarization)$/.test(message)) return { message }
903
+ return undefined
904
+ }
905
+ function useManualCompactionFailure (sessionId, command) {
906
+ const identity = JSON.stringify([sessionId, command?.commandId, command?.seq])
907
+ const [state, setState] = React.useState(null)
908
+ React.useEffect(() => {
909
+ if (!sessionId || !command?.commandId) return undefined
910
+ let cancelled = false
911
+ let release
912
+ const publish = (value) => { if (!cancelled) setState((previous) => value.error && previous?.identity === identity && previous.failure ? previous : { identity, ...value }) }
913
+ try {
914
+ const binding = sessionService?.binding(sessionId)
915
+ if (!binding?.eventSource) { publish({ missing: true }); return undefined }
916
+ const read = () => {
917
+ if (cancelled) return
918
+ try {
919
+ const events = (binding.eventSource.getSnapshot()?.entries ?? []).filter((entry) => entry.type === 'event').map((entry) => entry.event)
920
+ const own = events.filter((event) => event.data?.sourceCommandId === command.commandId)
921
+ const ids = new Set(own.filter((event) => event.type === 'compaction/start' || event.type === 'compaction/end').map((event) => event.data.compactionId).filter((id) => typeof id === 'string' && id))
922
+ const done = events.filter((event) => event.type === 'command/done' && event.data?.commandId === command.commandId)
923
+ const ends = ids.size === 1 && done.length <= 1 ? own.filter((event) => event.type === 'compaction/end' && ids.has(event.data.compactionId)
924
+ && validSeq(event.seq) && (!validSeq(command.seq) || event.seq >= command.seq)
925
+ && (done.length !== 1 || event.seq <= done[0].seq)) : []
926
+ const event = ends.length === 1 ? ends[0] : undefined
927
+ if (!event || typeof event.data.error !== 'string' || !event.data.error) { publish({ missing: true }); return }
928
+ const failure = safeCompactionFailure(event.data.error)
929
+ if (!failure) { publish({ missing: true }); return }
930
+ publish({ eventSeq: event.seq, compactionId: event.data.compactionId, time: event.time, failure })
931
+ } catch { publish({ error: true }) }
932
+ }
933
+ release = binding.eventSource.subscribe(read)
934
+ read()
935
+ if (validSeq(command.seq) && typeof binding.session?.loadThrough === 'function') {
936
+ Promise.resolve(binding.session.loadThrough(command.seq)).then(read, () => publish({ error: true }))
937
+ }
938
+ } catch { publish({ error: true }) }
939
+ return () => { cancelled = true; release?.() }
940
+ }, [identity])
941
+ return state?.identity === identity ? state : null
815
942
  }
816
943
  function CompactionErrorCard ({ title, fallback, sessionId, node, match, revision, t }) {
817
944
  const [open, setOpen] = React.useState(false)
818
- const [state, refresh] = useEvidence(open ? compactionUrl(sessionId) : '', revision)
945
+ const [state, refresh] = useEvidence(compactionUrl(sessionId), revision)
819
946
  const record = selectCompaction(state, match)
820
- return errorRow(title, { message: fallback }, state, {}, t, { sessionId, node, refresh }, {
947
+ const command = node?.data?.command
948
+ const nativeRecord = useManualCompactionFailure(sessionId, command)
949
+ const chosen = record?.attempts?.at(-1)
950
+ const protect = chosen?.failure?.code === 'AUTH' || chosen?.thrown?.code === 'AUTH' || chosen?.detail?.protected
951
+ const nativeFailure = nativeRecord?.failure ?? (command ? { message: fallback } : safeCompactionFailure(fallback))
952
+ const failure = protect ? { code: 'AUTH', message: '' } : nativeFailure ?? { message: t('compact.errorProtected') }
953
+ const selection = { chosen, unconfirmed: !!record }
954
+ return errorRow(title, failure, state, selection, t, { sessionId, node, refresh }, {
955
+ nativeStatus: !chosen?.detail && (chosen?.failure || chosen?.thrown) ? h('section', { className: 'bt_summaryFailure' },
956
+ h('h4', { className: 'bt_heading' }, t('report.summaryError') + ' · ' + t('ui.unconfirmed')),
957
+ protect ? h('div', { className: 'bt_protected' }, t('ui.authProtected'))
958
+ : originalControls(chosen.failure ?? chosen.thrown, t))
959
+ : chosen?.finishReason ? h('section', { className: 'bt_summaryFailure' },
960
+ h('h4', { className: 'bt_heading' }, t('report.summaryError') + ' · ' + t('ui.unconfirmed')),
961
+ h('code', { className: 'bt_meta' }, 'finish=' + chosen.finishReason))
962
+ : command && !nativeRecord?.failure ? h('div', { className: 'bt_meta' }, t(nativeRecord?.error ? 'compact.recordFailed' : 'compact.recordMissing')) : null,
963
+ fallbackNative: true,
821
964
  open, onToggle: (event) => { if (!event.target || event.target === event.currentTarget) setOpen(event.currentTarget.open) }, network: false,
822
- report: h(ReportButton, { key: JSON.stringify([sessionId, logCoordinate(node), revision]),
823
- url: compactionUrl(sessionId), state, failure: { message: fallback }, row: node?.data?.command ?? node?.data, title, t,
965
+ report: h(ReportButton, { key: JSON.stringify([sessionId, logCoordinate(node), revision, nativeRecord?.eventSeq, failure]),
966
+ url: compactionUrl(sessionId), state, failure, row: command ?? node?.data, title, t,
824
967
  select: (fresh) => {
825
968
  const matched = selectCompaction(fresh, match)
826
- const attempt = [...(matched?.attempts ?? [])].reverse().find((item) => item.failure || item.thrown)
969
+ const attempt = matched?.attempts?.at(-1)
827
970
  return { chosen: attempt, unconfirmed: !!matched, record: matched }
828
971
  } }),
829
972
  history: h(React.Fragment, null, compactionExtra(record, t),
@@ -896,7 +1039,8 @@ window.__ModuleLoader__.load({
896
1039
  for (const [key, component] of [
897
1040
  ['turn-error', TurnErrorRow], ['model-retry', ModelRetryRow], ['compaction', CompactionRow],
898
1041
  ['manual-compaction', ManualCompactionRow], [KIND, CompactionFailureRow],
899
- ]) yield ctx.slots.register({ name: 'conversation.chat.node', key, priority: -100, locale: NS }, component)
1042
+ ]) yield ctx.slots.register({ name: 'conversation.chat.node', key, priority: -100, locale: NS },
1043
+ key === 'compaction' ? component : (props) => h(ErrorSequenceRow, { ...props, component }))
900
1044
  })
901
1045
  },
902
1046
  }