dsh-tacit 0.3.0 → 0.5.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/client.js CHANGED
@@ -50,15 +50,19 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
50
50
  'status.autoOn': '自动学习已开启 · 今日 {today}/{budget}',
51
51
  'status.autoOff': '自动学习已关闭 — 仅在你点击「分析」时学习。',
52
52
  'status.autoHint': '混乱的轮次(重试、工具错误、压缩)以及你纠正智能体的下一条消息会被自动分析,无需点击。',
53
+ 'status.trendCorrections': '你纠正智能体:{a} → {b}',
53
54
  'status.trendMessy': '混乱轮次:{a} → {b}',
54
55
  'status.trendTokens': '每轮 tokens:{a} → {b}',
55
- 'status.trendHint': '(最早 {n} 轮 vs 最近 {n} 轮,真实数据)',
56
+ 'status.trendHint': '(最早 {n} 轮 vs 最近 {n} 轮,真实数据——这是你自己轮次的前后对比,不是对照实验)',
57
+ 'status.trendDocs': '试用与趋势的判定方式见《How it works》:§6 Trials https://github.com/hackernotfound/dsh-tacit/blob/main/docs/how-it-works.md#6-trials · §10 The measured trend https://github.com/hackernotfound/dsh-tacit/blob/main/docs/how-it-works.md#10-the-measured-trend',
56
58
  'turn.enrichment': '发送前补充的上下文',
57
59
  'trigger.auto': '自动',
58
60
  'trigger.correction': '纠正',
59
61
  'trigger.manual': '手动',
60
62
  'trigger.bootstrap': '引导',
61
63
  'trigger.good': '做对了',
64
+ 'trigger.feedback': '反馈',
65
+ 'trigger.send': '发送',
62
66
  'report.strengths': '这次做对了什么',
63
67
  'report.lesson': '经验',
64
68
  'settings.learnGood': '也从混乱之后的顺利提示词中学习',
@@ -80,6 +84,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
80
84
  'steer.remove': '删除',
81
85
  'steer.preview': '实际注入的文本',
82
86
  'steer.distilled': '自动提炼',
87
+ 'steer.queued': '等待试用',
83
88
  'steer.trial': '试用 {n}/{total}',
84
89
  'steer.active': '已生效',
85
90
  'steer.retired': '已淘汰 · {reason}',
@@ -87,6 +92,30 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
87
92
  'steer.workspace': '仅 {name}',
88
93
  'steer.scope': '适用范围',
89
94
  'steer.everywhere': '所有工作区',
95
+ 'steer.receipt': '这条指令从哪来',
96
+ 'steer.review': '新指令先经我确认再开始试用',
97
+ 'steer.startTrial': '开始试用',
98
+ 'steer.receiptLoading': '读取中…',
99
+ 'steer.receiptCopy': '复制为 JSON',
100
+ 'steer.receiptNever': '从未',
101
+ 'steer.receiptNoRun': '无记账记录',
102
+ 'steer.receiptId': '编号',
103
+ 'steer.receiptScope': '适用范围',
104
+ 'steer.receiptStatus': '状态',
105
+ 'steer.receiptSource': '来源',
106
+ 'steer.receiptCreated': '创建于',
107
+ 'steer.receiptUpdated': '更新于',
108
+ 'steer.receiptEvaluated': '评定于',
109
+ 'steer.receiptVersion': '版本',
110
+ 'steer.receiptTrial': '试用窗口',
111
+ 'steer.receiptTrialValue': '{turns} 轮,始于 {started} · 基线:杂乱 {messy}、纠正 {corrected}',
112
+ 'steer.receiptTriggers': '触发类别',
113
+ 'steer.receiptEvidence': '证据',
114
+ 'steer.receiptEvidenceValue': '{turns} 轮,来自 {conversations} 个会话',
115
+ 'steer.receiptCost': '提炼花费',
116
+ 'steer.receiptCostValue': '{usd} · {calls} 次调用',
117
+ 'steer.moveTo': '移到工作区',
118
+ 'steer.notSeen': '自 {date} 起未见',
90
119
  'settings.auto': '自动分析混乱轮次',
91
120
  'settings.budget': '每日自动分析上限',
92
121
  settings: '设置',
@@ -165,10 +194,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
165
194
  'usage.last30': '最近 30 天',
166
195
  'usage.since': '自 {day} 起',
167
196
  'usage.calls': '计费调用(30 天)',
197
+ 'usage.tokens': 'Tokens(30 天)',
168
198
  'usage.avgAnalysis': '单次分析中位成本',
169
199
  'usage.cachedRate': '输入缓存命中率(30 天)',
170
200
  'usage.unpriced': '无价目调用(30 天)',
171
201
  'usage.unpricedShort': '{n} 次无价目',
202
+ 'usage.failedRepair': '失败或修复调用花费(30 天)',
203
+ 'usage.failedRepairSplit': '失败 {failed} · 修复 {repair}',
204
+ 'usage.autoToday': '今日自动分析 {n} / {budget}',
172
205
  'usage.priceUnavailable': '无价目',
173
206
  'usage.chartLabel': '每日花费,最近 {n} 天',
174
207
  'usage.chartSummary': '最近 {n} 天:合计 {total};最高 {day} {max}',
@@ -176,6 +209,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
176
209
  'usage.series7': '7 天',
177
210
  'usage.series30': '30 天',
178
211
  'usage.breakdown': '按操作分类(最近 30 天)',
212
+ 'usage.byRoute': '按线路分类(最近 30 天)',
213
+ 'usage.byTrigger': '按触发方式分类(最近 30 天)',
179
214
  'usage.filters': '筛选',
180
215
  'usage.loading': '正在载入这次运行的调用明细…',
181
216
  'usage.page': '第 {page} / {pages} 页',
@@ -185,6 +220,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
185
220
  'usage.col.time': '时间',
186
221
  'usage.col.op': '操作',
187
222
  'usage.col.scope': '范围',
223
+ 'usage.scopeAll': '全部会话',
188
224
  'usage.col.model': '模型',
189
225
  'usage.col.status': '状态',
190
226
  'usage.col.calls': '计费调用',
@@ -236,6 +272,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
236
272
  'privacy.warnHint': '填 0 表示关闭提醒。达到阈值的 80% 时开始提醒,超过阈值后标记为已超出。',
237
273
  'privacy.clearUsage': '清除用量记录',
238
274
  'privacy.apply': '应用',
275
+ 'privacy.applyTo': '{action}:{field}',
239
276
  'confirm.reportsTitle': '清除所有分析报告?',
240
277
  'confirm.reportsBody': '这会删除 Tacit 为所有会话写下的全部分析报告。指令、风格规则和用量记录会保留。此操作无法撤销。',
241
278
  'confirm.usageTitle': '清除用量记录?',
@@ -280,6 +317,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
280
317
  'err.busy': '该轮次的分析已在运行中。',
281
318
  'err.no-llm': '无法访问模型服务。',
282
319
  'err.no-api-key': 'DeepSeek API Key 缺失或无效(请在 设置 → 模型 中配置)。',
320
+ 'err.no-credit': '模型服务商余额或配额已耗尽。',
283
321
  'err.rate-limited': '请求被限流,请稍后重试。',
284
322
  'err.timeout': '模型调用超时,请重试。',
285
323
  'err.empty-response': '模型没有返回内容,请重试。',
@@ -287,6 +325,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
287
325
  'err.network': '无法连接 Tacit 服务。',
288
326
  'err.internal': '内部错误:{detail}',
289
327
  'err.continuation': '这是一条纯粹的「继续」类消息(没有可分析的意图)——请分析它之前的那一轮。',
328
+ 'err.directive-policy': '指令不能改变工具权限、审批或沙箱设置,请换一种说法。',
329
+ 'err.already-analyzed': '这一轮已有报告;点击「重新分析」可再花一次费用分析。',
330
+ 'err.unknown-directive': '找不到这条指令。',
290
331
  }
291
332
 
292
333
  const DICT_EN = {
@@ -295,15 +336,19 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
295
336
  'status.autoOn': 'Auto-learning on · {today}/{budget} today',
296
337
  'status.autoOff': 'Auto-learning off — Tacit only learns when you click Analyze.',
297
338
  'status.autoHint': 'Messy turns (retries, tool errors, compactions) and turns you correct in your next message are analyzed automatically — no clicks.',
339
+ 'status.trendCorrections': 'You corrected the agent: {a} → {b}',
298
340
  'status.trendMessy': 'Messy turns: {a} → {b}',
299
341
  'status.trendTokens': 'Tokens/turn: {a} → {b}',
300
- 'status.trendHint': '(first {n} turns vs. latest {n}, measured)',
342
+ 'status.trendHint': '(first {n} turns vs. latest {n}, measured — before/after on your own turns, not a controlled comparison)',
343
+ 'status.trendDocs': 'How these are judged: §6 Trials https://github.com/hackernotfound/dsh-tacit/blob/main/docs/how-it-works.md#6-trials · §10 The measured trend https://github.com/hackernotfound/dsh-tacit/blob/main/docs/how-it-works.md#10-the-measured-trend',
301
344
  'turn.enrichment': 'Context added before the send',
302
345
  'trigger.auto': 'auto',
303
346
  'trigger.correction': 'correction',
304
347
  'trigger.manual': 'manual',
305
348
  'trigger.bootstrap': 'bootstrap',
306
349
  'trigger.good': 'what worked',
350
+ 'trigger.feedback': 'feedback',
351
+ 'trigger.send': 'send',
307
352
  'report.strengths': 'What this prompt got right',
308
353
  'report.lesson': 'Lesson',
309
354
  'settings.learnGood': 'Also learn from a clean prompt right after a messy turn',
@@ -325,6 +370,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
325
370
  'steer.remove': 'Remove',
326
371
  'steer.preview': 'Exact text injected',
327
372
  'steer.distilled': 'distilled',
373
+ 'steer.queued': 'waiting for trial',
328
374
  'steer.trial': 'trial {n}/{total}',
329
375
  'steer.active': 'active',
330
376
  'steer.retired': 'retired · {reason}',
@@ -332,6 +378,30 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
332
378
  'steer.workspace': 'only {name}',
333
379
  'steer.scope': 'Applies to',
334
380
  'steer.everywhere': 'Everywhere',
381
+ 'steer.receipt': 'Where this directive came from',
382
+ 'steer.review': 'Review new directives before their trial',
383
+ 'steer.startTrial': 'Start trial',
384
+ 'steer.receiptLoading': 'Reading…',
385
+ 'steer.receiptCopy': 'Copy as JSON',
386
+ 'steer.receiptNever': 'never',
387
+ 'steer.receiptNoRun': 'no ledger run',
388
+ 'steer.receiptId': 'Id',
389
+ 'steer.receiptScope': 'Applies to',
390
+ 'steer.receiptStatus': 'Status',
391
+ 'steer.receiptSource': 'Source',
392
+ 'steer.receiptCreated': 'Created',
393
+ 'steer.receiptUpdated': 'Updated',
394
+ 'steer.receiptEvaluated': 'Evaluated',
395
+ 'steer.receiptVersion': 'Version',
396
+ 'steer.receiptTrial': 'Trial window',
397
+ 'steer.receiptTrialValue': '{turns} turns from {started} · baseline messy {messy}, corrections {corrected}',
398
+ 'steer.receiptTriggers': 'Trigger categories',
399
+ 'steer.receiptEvidence': 'Evidence',
400
+ 'steer.receiptEvidenceValue': '{turns} turns across {conversations} conversations',
401
+ 'steer.receiptCost': 'Distillation cost',
402
+ 'steer.receiptCostValue': '{usd} · {calls} calls',
403
+ 'steer.moveTo': 'Move to workspace',
404
+ 'steer.notSeen': 'not seen since {date}',
335
405
  'settings.auto': 'Auto-analyze messy turns',
336
406
  'settings.budget': 'Daily cap on automatic analyses',
337
407
  settings: 'Settings',
@@ -410,10 +480,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
410
480
  'usage.last30': 'Last 30 days',
411
481
  'usage.since': 'Since {day}',
412
482
  'usage.calls': 'Billed calls (30 d)',
483
+ 'usage.tokens': 'Tokens (30 d)',
413
484
  'usage.avgAnalysis': 'Median cost per analysis',
414
485
  'usage.cachedRate': 'Cached input rate (30 d)',
415
486
  'usage.unpriced': 'Unpriced calls (30 d)',
416
487
  'usage.unpricedShort': '{n} unpriced',
488
+ 'usage.failedRepair': 'Spent on failed or repair calls (30 d)',
489
+ 'usage.failedRepairSplit': 'failed {failed} · repair {repair}',
490
+ 'usage.autoToday': 'automatic analyses today {n} / {budget}',
417
491
  'usage.priceUnavailable': 'No price',
418
492
  'usage.chartLabel': 'Daily spend, last {n} days',
419
493
  'usage.chartSummary': 'Last {n} days: total {total}; highest {day} {max}',
@@ -421,6 +495,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
421
495
  'usage.series7': '7 d',
422
496
  'usage.series30': '30 d',
423
497
  'usage.breakdown': 'By operation (last 30 days)',
498
+ 'usage.byRoute': 'By route (last 30 days)',
499
+ 'usage.byTrigger': 'By trigger (last 30 days)',
424
500
  'usage.filters': 'Filters',
425
501
  'usage.loading': 'Loading this run\u2019s calls…',
426
502
  'usage.page': 'Page {page} of {pages}',
@@ -430,6 +506,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
430
506
  'usage.col.time': 'Time',
431
507
  'usage.col.op': 'Operation',
432
508
  'usage.col.scope': 'Scope',
509
+ 'usage.scopeAll': 'all sessions',
433
510
  'usage.col.model': 'Model',
434
511
  'usage.col.status': 'Status',
435
512
  'usage.col.calls': 'Billed calls',
@@ -481,6 +558,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
481
558
  'privacy.warnHint': '0 turns the warning off. Tacit warns at 80 % of the amount and marks it exceeded above it.',
482
559
  'privacy.clearUsage': 'Clear usage history',
483
560
  'privacy.apply': 'Apply',
561
+ 'privacy.applyTo': '{action}: {field}',
484
562
  'confirm.reportsTitle': 'Clear all analysis reports?',
485
563
  'confirm.reportsBody': 'This deletes every analysis report Tacit has written, for every session. Your directives, style rules and usage history are kept. This cannot be undone.',
486
564
  'confirm.usageTitle': 'Clear usage history?',
@@ -525,6 +603,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
525
603
  'err.busy': 'An analysis for this turn is already running.',
526
604
  'err.no-llm': 'The model service is unavailable.',
527
605
  'err.no-api-key': 'The DeepSeek API key is missing or invalid (configure it in Settings → Models).',
606
+ 'err.no-credit': 'Your provider balance or quota is exhausted.',
528
607
  'err.rate-limited': 'The request was rate-limited; try again shortly.',
529
608
  'err.timeout': 'The model call timed out; please retry.',
530
609
  'err.empty-response': 'The model returned no content; please retry.',
@@ -532,6 +611,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
532
611
  'err.network': 'Could not reach the Tacit service.',
533
612
  'err.internal': 'Internal error: {detail}',
534
613
  'err.continuation': 'This is a bare continuation ("continue", "go ahead") with nothing to analyze — analyze the turn before it instead.',
614
+ 'err.directive-policy': 'A directive cannot change tool permissions, approvals or the sandbox. Reword it.',
615
+ 'err.already-analyzed': 'This turn already has a report; Re-analyze pays for a fresh one.',
616
+ 'err.unknown-directive': 'No directive with that id.',
535
617
  }
536
618
 
537
619
  // ── API client (the host half's own routes on the harness origin) ──────
@@ -631,6 +713,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
631
713
  auto: null,
632
714
  steering: null, // {enabled, text}
633
715
  workspaces: [], // [{cwd, label}] from /state
716
+ receipts: {}, // directive id → the /directive-receipt receipt, re-read on each expand
634
717
  trend: null, // measured early-vs-recent trend
635
718
  bootstrap: null, // {running, done, total}
636
719
  coached: [], // cross-session coached-prompt entries
@@ -655,7 +738,6 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
655
738
  usageFilters: { range: '30d', type: '', status: '', model: '', workspace: '', sessionId: '', page: 1, pageSize: 20 },
656
739
  usageRuns: {}, // runId → the full run (attempts included), fetched on first expand
657
740
  usageExpanded: new Set(), // runIds whose attempt rows are open
658
- usageLoading: false,
659
741
  usageSeries: '30', // '7' | '30' — which sparkline the strip shows
660
742
  pricingRefreshing: false, // a /pricing-refresh call is in flight
661
743
  initStarted: false,
@@ -775,14 +857,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
775
857
 
776
858
  /** Read the whole cost panel in one call; a failure keeps the last envelope. */
777
859
  async function fetchUsage() {
778
- rootStore.usageLoading = true
779
860
  try {
780
861
  const result = await api('/usage', usageQuery())
781
862
  if (result !== null && typeof result === 'object' && result.ok === true) rootStore.usage = result
782
863
  } catch {
783
864
  // A stale panel beats a blank one; the next poll tries again.
784
865
  }
785
- rootStore.usageLoading = false
786
866
  notifyRoot()
787
867
  }
788
868
 
@@ -1022,13 +1102,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1022
1102
  function closeConfirm() {
1023
1103
  rootStore.confirm = null
1024
1104
  notifyRoot()
1105
+ restoreConfirmOpener()
1025
1106
  }
1026
1107
 
1027
1108
  /** Run the parked destructive action, whichever it is, and close the dialog. */
1028
1109
  async function confirmAction() {
1029
1110
  const kind = rootStore.confirm !== null && typeof rootStore.confirm === 'object' ? rootStore.confirm.kind : null
1030
- rootStore.confirm = null
1031
- notifyRoot()
1111
+ closeConfirm()
1032
1112
  if (kind === 'reports') await clearAllRoot()
1033
1113
  else if (kind === 'usage') await clearUsageHistory()
1034
1114
  }
@@ -1144,6 +1224,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1144
1224
  notifyRoot()
1145
1225
  }
1146
1226
 
1227
+ /** One directive's audit receipt. Re-read on every expand: a trial's counters move while it runs. */
1228
+ async function fetchReceipt(id) {
1229
+ const key = String(id)
1230
+ if (key.length === 0) return
1231
+ rootStore.error = null
1232
+ try {
1233
+ const result = await api('/directive-receipt', { id: key })
1234
+ if (result !== null && typeof result === 'object' && result.ok === true
1235
+ && result.receipt !== null && typeof result.receipt === 'object') {
1236
+ rootStore.receipts[key] = result.receipt
1237
+ } else {
1238
+ rootStore.error = { code: result !== null && typeof result === 'object' && typeof result.code === 'string' ? result.code : 'bad-request', detail: '' }
1239
+ }
1240
+ } catch (error) {
1241
+ rootStore.error = errorOf(error)
1242
+ }
1243
+ notifyRoot()
1244
+ }
1245
+
1147
1246
  async function initStore(store) {
1148
1247
  if (store === null || store.initStarted) return
1149
1248
  store.initStarted = true
@@ -1174,7 +1273,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1174
1273
  store.error = null
1175
1274
  store.notice = null
1176
1275
  notify(store)
1177
- api('/analyze', { sessionId: store.sessionId, turn })
1276
+ // A turn that already has a report is only re-analyzed on purpose; the button reads "Re-analyze" then.
1277
+ api('/analyze', { sessionId: store.sessionId, turn, ...(store.reports[String(turn)] ? { force: true } : {}) })
1178
1278
  .then((result) => {
1179
1279
  if (result !== null && typeof result === 'object' && result.ok && result.report !== null && typeof result.report === 'object') {
1180
1280
  store.reports[String(turn)] = result.report
@@ -1238,7 +1338,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1238
1338
  // `busy` means an analysis of that turn was already running — a
1239
1339
  // race with the auto-analyzer, not a failure of this batch.
1240
1340
  const code = typeof entry.code === 'string' && entry.code.length > 0 ? entry.code : 'call-failed'
1241
- if (failure === null && code !== 'busy') failure = { code, detail: '' }
1341
+ if (failure === null && code !== 'busy' && code !== 'already-analyzed') failure = { code, detail: '' }
1242
1342
  }
1243
1343
  if (failure !== null) store.error = failure
1244
1344
  if (result.profile !== null && typeof result.profile === 'object') store.profile = result.profile
@@ -1568,14 +1668,30 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1568
1668
  }
1569
1669
 
1570
1670
  /**
1571
- * Billed tokens in one bucket set: uncached input + output + cache reads +
1572
- * cache writes. `reasoningTokens` is a subset of `outputTokens`, so adding
1573
- * it would count every reasoning token twice.
1671
+ * Billed tokens in one bucket set, split the way they are priced:
1672
+ * everything the prompt cost (uncached input, cache reads, cache writes)
1673
+ * against what came back. `reasoningTokens` is a subset of `outputTokens`,
1674
+ * so adding it would count every reasoning token twice.
1574
1675
  */
1575
- function tokensTotal(buckets) {
1576
- if (buckets === null || typeof buckets !== 'object') return 0
1676
+ function tokensInOut(buckets) {
1677
+ if (buckets === null || typeof buckets !== 'object') return { input: 0, output: 0 }
1577
1678
  const num = (value) => (typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0)
1578
- return num(buckets.inputTokens) + num(buckets.outputTokens) + num(buckets.cacheReadTokens) + num(buckets.cacheWriteTokens)
1679
+ return {
1680
+ input: num(buckets.inputTokens) + num(buckets.cacheReadTokens) + num(buckets.cacheWriteTokens),
1681
+ output: num(buckets.outputTokens),
1682
+ }
1683
+ }
1684
+
1685
+ /** Both halves of `tokensInOut` at once. */
1686
+ function tokensTotal(buckets) {
1687
+ const split = tokensInOut(buckets)
1688
+ return split.input + split.output
1689
+ }
1690
+
1691
+ /** `tokensInOut` as the one line the tiles and the run rows both show. */
1692
+ function fmtTokensSplit(buckets) {
1693
+ const split = tokensInOut(buckets)
1694
+ return 'in ' + fmtTokens(split.input) + ' · out ' + fmtTokens(split.output)
1579
1695
  }
1580
1696
 
1581
1697
  function usageNum(value) {
@@ -1593,7 +1709,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1593
1709
  }
1594
1710
  }
1595
1711
 
1596
- /** One period card, zero-filled; the two derived figures stay nullable. */
1712
+ /**
1713
+ * One period card, zero-filled; the two averages stay nullable.
1714
+ * `failedUsd` and `repairUsd` overlap on a failed repair, so they can sum
1715
+ * past `failedOrRepairUsd`, which counts that union once.
1716
+ */
1597
1717
  function usagePeriod(value) {
1598
1718
  const source = value !== null && typeof value === 'object' ? value : {}
1599
1719
  return {
@@ -1601,8 +1721,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1601
1721
  billedCalls: usageNum(source.billedCalls),
1602
1722
  unmeteredCalls: usageNum(source.unmeteredCalls),
1603
1723
  unpricedCalls: usageNum(source.unpricedCalls),
1724
+ failedCalls: usageNum(source.failedCalls),
1604
1725
  tokens: usageTokens(source.tokens),
1605
1726
  usdKnown: usageNum(source.usdKnown),
1727
+ failedUsd: usageNum(source.failedUsd),
1728
+ repairUsd: usageNum(source.repairUsd),
1729
+ failedOrRepairUsd: usageNum(source.failedOrRepairUsd),
1606
1730
  avgAnalysisUsd: typeof source.avgAnalysisUsd === 'number' && Number.isFinite(source.avgAnalysisUsd) ? source.avgAnalysisUsd : null,
1607
1731
  cachedInputRate: typeof source.cachedInputRate === 'number' && Number.isFinite(source.cachedInputRate) ? source.cachedInputRate : null,
1608
1732
  }
@@ -1639,8 +1763,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1639
1763
  billedCalls: usageNum(source.billedCalls),
1640
1764
  unmeteredCalls: usageNum(source.unmeteredCalls),
1641
1765
  unpricedCalls: usageNum(source.unpricedCalls),
1766
+ failedCalls: usageNum(source.failedCalls),
1642
1767
  tokens: usageTokens(source.tokens),
1643
1768
  usdKnown: usageNum(source.usdKnown),
1769
+ failedUsd: usageNum(source.failedUsd),
1644
1770
  trigger: text('trigger'),
1645
1771
  startedAt: usageNum(source.startedAt),
1646
1772
  endedAt: usageNum(source.endedAt),
@@ -1661,6 +1787,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1661
1787
  if (value === null || typeof value !== 'object' || value.ok !== true) return null
1662
1788
  const runs = value.runs !== null && typeof value.runs === 'object' ? value.runs : {}
1663
1789
  const warnings = value.warnings !== null && typeof value.warnings === 'object' ? value.warnings : {}
1790
+ const auto = value.auto !== null && typeof value.auto === 'object' ? value.auto : {}
1664
1791
  return {
1665
1792
  trackingSince: usageNum(value.trackingSince),
1666
1793
  pricing: value.pricing !== null && typeof value.pricing === 'object' ? value.pricing : {},
@@ -1671,9 +1798,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1671
1798
  lifetime: usagePeriod(value.lifetime),
1672
1799
  byType: usageBuckets(value.byType),
1673
1800
  byModel: usageBuckets(value.byModel),
1801
+ byProvider: usageBuckets(value.byProvider),
1802
+ byTrigger: usageBuckets(value.byTrigger),
1674
1803
  series7: usageSeries(value.series7),
1675
1804
  series30: usageSeries(value.series30),
1676
1805
  warnings: { daily: usageWarning(warnings.daily), monthly: usageWarning(warnings.monthly) },
1806
+ auto: { today: usageNum(auto.today), budget: usageNum(auto.budget) },
1677
1807
  runs: {
1678
1808
  items: (Array.isArray(runs.items) ? runs.items : []).map(usageRunItem),
1679
1809
  page: usageNum(runs.page) > 0 ? Math.floor(usageNum(runs.page)) : 1,
@@ -1751,7 +1881,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1751
1881
  h('span', { className: 'tacit-progress-count' }, autoOn ? t('status.autoOn', { today: String(today), budget: String(budget) }) : '')),
1752
1882
  h('div', { className: 'tacit-progress-text' }, autoOn ? t('status.autoHint') : t('status.autoOff')),
1753
1883
  hasTrend
1754
- ? h('div', { className: 'tacit-trend' },
1884
+ ? h('div', { className: 'tacit-trend', title: t('status.trendDocs') },
1885
+ h('span', { className: 'tacit-chip' }, t('status.trendCorrections', { a: pct(trend.early.correctionRate), b: pct(trend.recent.correctionRate) })),
1755
1886
  h('span', { className: 'tacit-chip' }, t('status.trendMessy', { a: pct(trend.early.messyRate), b: pct(trend.recent.messyRate) })),
1756
1887
  h('span', { className: 'tacit-chip' }, t('status.trendTokens', { a: fmt(trend.early.tokensPerTurn), b: fmt(trend.recent.tokensPerTurn) })),
1757
1888
  h('span', { className: 'tacit-progress-text' }, t('status.trendHint', { n: String(trend.window) })))
@@ -2249,42 +2380,67 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2249
2380
  *
2250
2381
  * It is captured by the store action that opens the dialog rather than by
2251
2382
  * the dialog itself: React focuses the `autoFocus`ed Cancel button while it
2252
- * commits, so by the time an effect runs the opener is already gone.
2383
+ * commits, so by the time the dialog is mounted the opener is already gone.
2253
2384
  */
2254
2385
  let confirmOpener = null
2255
2386
 
2387
+ /**
2388
+ * The two focus stops of the dialog, addressed by id rather than by ref:
2389
+ * refs would add hooks to a component whose hook count must not move.
2390
+ */
2391
+ const CONFIRM_CANCEL_ID = 'tacit-confirm-cancel'
2392
+ const CONFIRM_ACCEPT_ID = 'tacit-confirm-accept'
2393
+
2256
2394
  function captureConfirmOpener() {
2257
2395
  confirmOpener = typeof document !== 'undefined' && document !== null ? document.activeElement : null
2258
2396
  }
2259
2397
 
2398
+ /**
2399
+ * Hand focus back one macrotask after the close. The host's Settings modal
2400
+ * moves focus on the same Escape, so a restore that runs inside the close is
2401
+ * overwritten and the user lands on the host's Close button instead.
2402
+ */
2403
+ function restoreConfirmOpener() {
2404
+ const opener = confirmOpener
2405
+ confirmOpener = null
2406
+ if (opener === null || typeof opener.focus !== 'function') return
2407
+ setTimeout(() => opener.focus(), 0)
2408
+ }
2409
+
2260
2410
  /**
2261
2411
  * One small modal confirmation, rendered unconditionally by its owner (and
2262
2412
  * `null` when closed) so React's hook count never changes between renders.
2263
- * Hook-free but for the focus restore, which is the one thing it cannot do
2264
- * with markup alone.
2413
+ * Hook-free: focus is restored by `closeConfirm`, the one path out.
2265
2414
  *
2266
2415
  * Cancel comes first and takes focus: the safe answer is the one a keyboard
2267
2416
  * or screen-reader user reaches without looking.
2268
2417
  */
2269
2418
  function ConfirmDialog(kit, { open, title, body, confirmLabel, onConfirm, onCancel }) {
2270
2419
  const { t } = kit
2271
- const isOpen = open === true
2272
- useEffect(() => {
2273
- if (!isOpen || typeof document === 'undefined') return undefined
2274
- const opener = confirmOpener
2275
- return () => {
2276
- if (opener !== null && typeof opener.focus === 'function') opener.focus()
2277
- }
2278
- }, [isOpen])
2279
- if (!isOpen) return null
2420
+ if (open !== true) return null
2280
2421
  const cancel = typeof onCancel === 'function' ? onCancel : () => {}
2281
2422
  return h('div', {
2282
2423
  className: 'tacit-modal-backdrop',
2283
2424
  onClick: cancel,
2284
- // Escape cancels from anywhere inside: keydown from either button
2285
- // bubbles here.
2425
+ // Escape and Tab are both handled here: a keydown from either button
2426
+ // bubbles to the backdrop. With exactly two stops in the dialog,
2427
+ // forward and backward tabbing both land on the other button, so
2428
+ // `shiftKey` needs no branch of its own.
2286
2429
  onKeyDown: (event) => {
2287
- if (event.key === 'Escape') cancel()
2430
+ if (event.key === 'Escape') {
2431
+ // Stopped here: the host's Settings modal also closes on Escape, and
2432
+ // cancelling a confirm must not take the whole settings page with it.
2433
+ if (typeof event.stopPropagation === 'function') event.stopPropagation()
2434
+ cancel()
2435
+ return
2436
+ }
2437
+ if (event.key !== 'Tab' || typeof document === 'undefined' || document === null) return
2438
+ const cancelButton = document.getElementById(CONFIRM_CANCEL_ID)
2439
+ const acceptButton = document.getElementById(CONFIRM_ACCEPT_ID)
2440
+ if (cancelButton === null || acceptButton === null) return
2441
+ const next = document.activeElement === acceptButton ? cancelButton : acceptButton
2442
+ event.preventDefault()
2443
+ if (typeof next.focus === 'function') next.focus()
2288
2444
  },
2289
2445
  },
2290
2446
  h('div', {
@@ -2298,17 +2454,76 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2298
2454
  h('h3', { className: 'tacit-modal-title', id: 'tacit-confirm-title' }, title),
2299
2455
  h('p', { className: 'tacit-confirm-body' }, body),
2300
2456
  h('div', { className: 'tacit-confirm-actions' },
2301
- h('button', { type: 'button', className: 'tacit-btn', autoFocus: true, onClick: cancel }, t('confirm.cancel')),
2302
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-danger', onClick: onConfirm }, confirmLabel))))
2457
+ h('button', { type: 'button', id: CONFIRM_CANCEL_ID, className: 'tacit-btn', autoFocus: true, onClick: cancel }, t('confirm.cancel')),
2458
+ h('button', { type: 'button', id: CONFIRM_ACCEPT_ID, className: 'tacit-btn tacit-btn-danger', onClick: onConfirm }, confirmLabel))))
2303
2459
  }
2304
2460
 
2305
2461
  // ── Global panel (Settings → Tacit section) ────────────────────────────
2306
2462
 
2307
2463
  function DirectivesEditor(kit) {
2308
2464
  const { t } = kit
2465
+ const fmtDay = (ms) => {
2466
+ const date = new Date(Number(ms))
2467
+ return !(Number(ms) > 0) || Number.isNaN(date.getTime()) ? t('steer.receiptNever') : date.toISOString().slice(0, 10)
2468
+ }
2469
+ const pct = (value) => (typeof value === 'number' && value >= 0 ? Math.round(value * 100) + '%' : t('steer.receiptNever'))
2470
+ const triggerName = (name) => {
2471
+ const key = 'trigger.' + name
2472
+ const label = t(key)
2473
+ return label === key ? name : label
2474
+ }
2475
+ const copyReceipt = (receipt) => {
2476
+ if (typeof navigator === 'undefined' || navigator === null || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return
2477
+ navigator.clipboard.writeText(JSON.stringify(receipt, null, 2)).catch(() => {})
2478
+ }
2479
+ const receiptRow = (label, value) => [h('dt', { key: label + '-t' }, label), h('dd', { key: label + '-d' }, value)]
2480
+ /** What the receipt route returned for this directive, as definition rows; never any prompt text. */
2481
+ function Receipt(receipt) {
2482
+ if (receipt === null || typeof receipt === 'object' === false) return h('div', { className: 'tacit-panel-hint' }, t('steer.receiptLoading'))
2483
+ const triggers = Object.entries(receipt.triggers !== null && typeof receipt.triggers === 'object' ? receipt.triggers : {})
2484
+ .map(([name, count]) => triggerName(name) + ' ' + String(count)).join(' · ')
2485
+ const evidence = receipt.evidence !== null && typeof receipt.evidence === 'object' ? receipt.evidence : { turns: 0, conversations: 0 }
2486
+ const cost = receipt.cost !== null && typeof receipt.cost === 'object' ? receipt.cost : { usd: null, calls: 0 }
2487
+ const trial = receipt.trial !== null && typeof receipt.trial === 'object' ? receipt.trial : null
2488
+ return h('div', null,
2489
+ h('dl', { className: 'tacit-receipt' },
2490
+ ...receiptRow(t('steer.receiptId'), String(receipt.id)),
2491
+ ...receiptRow(t('steer.receiptScope'), typeof receipt.scope === 'string' && receipt.scope.length > 0 ? receipt.scope : t('steer.everywhere')),
2492
+ ...receiptRow(t('steer.receiptStatus'), String(receipt.status)),
2493
+ ...receiptRow(t('steer.receiptSource'), receipt.source === 'user' ? t('steer.user') : t('steer.distilled')),
2494
+ ...receiptRow(t('steer.receiptCreated'), fmtDay(receipt.createdAt)),
2495
+ ...receiptRow(t('steer.receiptUpdated'), fmtDay(receipt.updatedAt)),
2496
+ ...receiptRow(t('steer.receiptEvaluated'), fmtDay(receipt.evaluatedAt)),
2497
+ ...receiptRow(t('steer.receiptVersion'), String(receipt.version)),
2498
+ ...receiptRow(t('steer.receiptTrial'), trial === null ? t('steer.receiptNever') : t('steer.receiptTrialValue', {
2499
+ turns: String(trial.turns), started: fmtDay(trial.startedAt), messy: pct(trial.baselineMessyRate), corrected: pct(trial.baselineCorrectionRate) })),
2500
+ ...receiptRow(t('steer.receiptTriggers'), triggers.length > 0 ? triggers : t('steer.receiptNever')),
2501
+ ...receiptRow(t('steer.receiptEvidence'), t('steer.receiptEvidenceValue', { turns: String(evidence.turns), conversations: String(evidence.conversations) })),
2502
+ ...receiptRow(t('steer.receiptCost'), cost.usd === null ? t('steer.receiptNoRun') : t('steer.receiptCostValue', { usd: fmtUsd(cost.usd), calls: String(cost.calls) }))),
2503
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => copyReceipt(receipt) }, t('steer.receiptCopy')))
2504
+ }
2309
2505
  return function DirectivesEditorView(props) {
2310
- const { config, directives, steering } = props
2506
+ const { config, steering } = props
2507
+ const directives = (Array.isArray(props.directives) ? props.directives : []).filter((entry) => entry.status !== 'removed')
2508
+ const receipts = rootStore.receipts !== null && typeof rootStore.receipts === 'object' ? rootStore.receipts : {}
2509
+ const reviewOn = config !== null && config.reviewCandidates === true
2311
2510
  const workspaces = Array.isArray(props.workspaces) ? props.workspaces : []
2511
+ const seenAt = props.seenAt !== null && typeof props.seenAt === 'object' ? props.seenAt : {}
2512
+ const liveScope = (scope) => workspaces.some((entry) => entry.cwd === scope || String(entry.cwd).startsWith(scope + '/'))
2513
+ const notSeenSince = (entry) => {
2514
+ const stamp = typeof seenAt[entry.workspace] === 'number' ? seenAt[entry.workspace] : entry.createdAt
2515
+ const date = new Date(Number(stamp))
2516
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
2517
+ }
2518
+ const scopeOptions = (current) => {
2519
+ const options = [
2520
+ h('option', { key: '-', value: '-' }, t('steer.moveTo')),
2521
+ h('option', { key: '', value: '' }, t('steer.everywhere')),
2522
+ ]
2523
+ for (const entry of workspaces) options.push(h('option', { key: entry.cwd, value: entry.cwd }, entry.label))
2524
+ if (current.length > 0 && !workspaces.some((entry) => entry.cwd === current)) options.push(h('option', { key: current, value: current }, labelOf(current)))
2525
+ return options
2526
+ }
2312
2527
  const [draft, setDraft] = useState('')
2313
2528
  const [scope, setScope] = useState('')
2314
2529
  const labelOf = (cwd) => {
@@ -2331,6 +2546,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2331
2546
  setEnrichOn(next)
2332
2547
  updateRootConfig({ enrichPrompts: next })
2333
2548
  }
2549
+ const toggleReview = () => updateRootConfig({ reviewCandidates: !reviewOn })
2334
2550
  const onAdd = () => {
2335
2551
  const text = draft.trim()
2336
2552
  if (text.length === 0) return
@@ -2346,10 +2562,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2346
2562
  h('div', { className: 'tacit-settings-row' },
2347
2563
  h('label', { className: 'tacit-settings-label' }, t('steer.enrich')),
2348
2564
  h('input', { type: 'checkbox', checked: enrichOn, onChange: toggleEnrich })),
2565
+ h('div', { className: 'tacit-settings-row' },
2566
+ h('label', { className: 'tacit-settings-label' }, t('steer.review')),
2567
+ h('input', { type: 'checkbox', checked: reviewOn, onChange: toggleReview })),
2349
2568
  directives.length === 0
2350
2569
  ? h('div', { className: 'tacit-empty' }, t('steer.empty'))
2351
2570
  : h('div', { className: 'tacit-rules-list' },
2352
- directives.map((entry) => h('div', { key: entry.id, className: 'tacit-rule tacit-directive' + (entry.enabled === false ? ' tacit-directive-off' : '') },
2571
+ directives.map((entry) => h('div', { key: entry.id, className: 'tacit-directive-block' },
2572
+ h('div', { className: 'tacit-rule tacit-directive' + (entry.enabled === false ? ' tacit-directive-off' : '') },
2353
2573
  h('input', {
2354
2574
  type: 'checkbox',
2355
2575
  className: 'tacit-check',
@@ -2362,15 +2582,40 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2362
2582
  typeof entry.workspace === 'string' && entry.workspace.length > 0
2363
2583
  ? h('span', { className: 'tacit-chip tacit-chip-scope', title: entry.workspace }, t('steer.workspace', { name: labelOf(entry.workspace) }))
2364
2584
  : null,
2365
- entry.status === 'candidate'
2366
- ? h('span', { className: 'tacit-chip tacit-chip-trial' }, t('steer.trial', {
2367
- n: String(entry.trial !== null && typeof entry.trial === 'object' && typeof entry.trial.turns === 'number' ? entry.trial.turns : 0),
2368
- total: String(config !== null && typeof config.directiveTrialTurns === 'number' ? config.directiveTrialTurns : 10),
2369
- }))
2370
- : entry.status === 'retired'
2371
- ? h('span', { className: 'tacit-chip tacit-chip-warn' }, t('steer.retired', { reason: String(entry.retiredReason || '') }))
2372
- : h('span', { className: 'tacit-chip tacit-chip-ok' }, t('steer.active')),
2373
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'remove', id: entry.id }) }, t('steer.remove'))))),
2585
+ typeof entry.workspace === 'string' && entry.workspace.length > 0 && !liveScope(entry.workspace)
2586
+ ? h('span', { className: 'tacit-chip tacit-chip-muted', title: entry.workspace }, t('steer.notSeen', { date: notSeenSince(entry) }))
2587
+ : null,
2588
+ entry.status === 'queued'
2589
+ ? h('span', { className: 'tacit-chip tacit-chip-muted' }, t('steer.queued'))
2590
+ : entry.status === 'candidate'
2591
+ ? h('span', { className: 'tacit-chip tacit-chip-trial' }, t('steer.trial', {
2592
+ n: String(entry.trial !== null && typeof entry.trial === 'object' && typeof entry.trial.turns === 'number' ? entry.trial.turns : 0),
2593
+ total: String(config !== null && typeof config.directiveTrialTurns === 'number' ? config.directiveTrialTurns : 10),
2594
+ }))
2595
+ : entry.status === 'retired'
2596
+ ? h('span', { className: 'tacit-chip tacit-chip-warn' }, t('steer.retired', { reason: String(entry.retiredReason || '') }))
2597
+ : h('span', { className: 'tacit-chip tacit-chip-ok' }, t('steer.active')),
2598
+ reviewOn && entry.status === 'queued' && !(entry.approvedAt > 0)
2599
+ ? h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'start-trial', id: entry.id }) }, t('steer.startTrial'))
2600
+ : null,
2601
+ h('select', {
2602
+ className: 'tacit-input tacit-select tacit-select-sm',
2603
+ 'aria-label': t('steer.moveTo'),
2604
+ onChange: (event) => {
2605
+ if (event.target.value !== '-') editDirectives({ action: 'rescope', id: entry.id, workspace: event.target.value })
2606
+ },
2607
+ }, ...scopeOptions(typeof entry.workspace === 'string' ? entry.workspace : '')),
2608
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'remove', id: entry.id }) }, t('steer.remove'))),
2609
+ h('details', {
2610
+ className: 'tacit-preview',
2611
+ 'data-testid': 'tacit-receipt',
2612
+ 'data-directive-id': entry.id,
2613
+ onToggle: (event) => {
2614
+ if (event.target.open) fetchReceipt(entry.id)
2615
+ },
2616
+ },
2617
+ h('summary', null, t('steer.receipt')),
2618
+ Receipt(receipts[entry.id] ?? null))))),
2374
2619
  h('div', { className: 'tacit-settings-row' },
2375
2620
  h('input', {
2376
2621
  className: 'tacit-input tacit-directive-input',
@@ -2515,9 +2760,15 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2515
2760
  const confirm = rootStore.confirm !== null && typeof rootStore.confirm === 'object' && rootStore.confirm.kind === 'usage'
2516
2761
  ? 'usage'
2517
2762
  : (rootStore.confirm !== null && typeof rootStore.confirm === 'object' && rootStore.confirm.kind === 'reports' ? 'reports' : null)
2518
- const retention = config !== null && typeof config.costHistoryDays === 'number' && RETENTION_DAYS.includes(config.costHistoryDays)
2763
+ const retention = config !== null && typeof config.costHistoryDays === 'number' && Number.isFinite(config.costHistoryDays) && config.costHistoryDays > 0
2519
2764
  ? config.costHistoryDays
2520
2765
  : 30
2766
+ // A value set by hand in the YAML need not be one of the offered days.
2767
+ // It gets an option of its own, in place, rather than being displayed
2768
+ // as a neighbour it is not.
2769
+ const retentionDays = RETENTION_DAYS.includes(retention)
2770
+ ? RETENTION_DAYS
2771
+ : [...RETENTION_DAYS, retention].sort((a, b) => a - b)
2521
2772
  /** Every card is titled by `card.<id>` and driven by `rootStore.sections`. */
2522
2773
  const card = (id, children, extra) => SectionCard(kit, {
2523
2774
  id,
@@ -2584,7 +2835,6 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2584
2835
  card('pricing', [
2585
2836
  PricingCard(kit, {
2586
2837
  pricing: usagePricing,
2587
- open: sections.pricing === true,
2588
2838
  refreshing: rootStore.pricingRefreshing === true,
2589
2839
  onRefresh: () => refreshPricing(t),
2590
2840
  }),
@@ -2618,7 +2868,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2618
2868
  ]),
2619
2869
  card('guidance', [
2620
2870
  h(DirectivesEditorView, {
2621
- workspaces: rootStore.workspaces, config, directives, steering: rootStore.steering }),
2871
+ workspaces: rootStore.workspaces, config, directives, steering: rootStore.steering,
2872
+ seenAt: rootStore.profile !== null && typeof rootStore.profile === 'object' ? rootStore.profile.workspaceSeenAt : null }),
2622
2873
  ]),
2623
2874
  card('improve', [
2624
2875
  h('div', { className: 'tacit-settings-row' },
@@ -2661,7 +2912,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2661
2912
  value: String(retention),
2662
2913
  onChange: (event) => updateRootConfig({ costHistoryDays: Number(event.target.value) }),
2663
2914
  },
2664
- ...RETENTION_DAYS.map((days) => h('option', { key: days, value: String(days) }, String(days))))),
2915
+ ...retentionDays.map((days) => h('option', { key: days, value: String(days) }, String(days))))),
2665
2916
  h('div', { className: 'tacit-settings-row' },
2666
2917
  h('label', { className: 'tacit-settings-label', htmlFor: 'tacit-warn-daily' }, t('privacy.warnDaily')),
2667
2918
  h('input', {
@@ -2678,7 +2929,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2678
2929
  className: 'tacit-btn tacit-btn-sm',
2679
2930
  // Two identically labelled buttons in one card: the accessible
2680
2931
  // name has to say which threshold each one applies.
2681
- 'aria-label': t('privacy.apply') + ': ' + t('privacy.warnDaily'),
2932
+ 'aria-label': t('privacy.applyTo', { action: t('privacy.apply'), field: t('privacy.warnDaily') }),
2682
2933
  onClick: () => applyWarn('costWarnDailyUsd', dailyText, setDailyText),
2683
2934
  }, t('privacy.apply'))),
2684
2935
  h('div', { className: 'tacit-settings-row' },
@@ -2697,7 +2948,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2697
2948
  className: 'tacit-btn tacit-btn-sm',
2698
2949
  // Two identically labelled buttons in one card: the accessible
2699
2950
  // name has to say which threshold each one applies.
2700
- 'aria-label': t('privacy.apply') + ': ' + t('privacy.warnMonthly'),
2951
+ 'aria-label': t('privacy.applyTo', { action: t('privacy.apply'), field: t('privacy.warnMonthly') }),
2701
2952
  onClick: () => applyWarn('costWarnMonthlyUsd', monthlyText, setMonthlyText),
2702
2953
  }, t('privacy.apply'))),
2703
2954
  h('p', { className: 'tacit-panel-hint' }, t('privacy.warnHint')),
@@ -2744,7 +2995,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2744
2995
  /** Every run type and op the ledger can tag, in the order the filters list them. */
2745
2996
  const USAGE_RUN_TYPES = ['bootstrap', 'analysis', 'analysis-batch', 'improve', 'directive-distillation', 'style-distillation', 'prompt-enrichment']
2746
2997
  const USAGE_RANGES = ['today', '7d', '30d', 'month', 'all']
2747
- const USAGE_STATUSES = ['success', 'partial', 'failed']
2998
+ const USAGE_STATUSES = ['running', 'success', 'partial', 'failed']
2748
2999
  const USAGE_COLUMNS = ['time', 'op', 'scope', 'model', 'status', 'calls', 'tokens', 'cost']
2749
3000
 
2750
3001
  /** One label/value tile; `note` is the small "n unpriced" line under the value. */
@@ -2755,10 +3006,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2755
3006
  typeof note === 'string' && note.length > 0 ? h('span', { className: 'tacit-tile-note' }, note) : null)
2756
3007
  }
2757
3008
 
2758
- /** Money tile: the period's spend plus its unpriced-call footnote. */
2759
- function SpendTile(kit, id, label, totals) {
2760
- const note = totals.unpricedCalls > 0 ? kit.t('usage.unpricedShort', { n: String(totals.unpricedCalls) }) : ''
2761
- return UsageTile(id, label, usageSpend(kit, totals), note)
3009
+ /** Money tile: the period's spend, its unpriced-call footnote, and any second note that period carries. */
3010
+ function SpendTile(kit, id, label, totals, extraNote) {
3011
+ const notes = []
3012
+ if (totals.unpricedCalls > 0) notes.push(kit.t('usage.unpricedShort', { n: String(totals.unpricedCalls) }))
3013
+ if (typeof extraNote === 'string' && extraNote.length > 0) notes.push(extraNote)
3014
+ return UsageTile(id, label, usageSpend(kit, totals), notes.join(' · '))
2762
3015
  }
2763
3016
 
2764
3017
  /**
@@ -2821,18 +3074,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2821
3074
  seriesButton('30', t('usage.series30'))))
2822
3075
  }
2823
3076
 
2824
- /** Spend per operation, costliest first, each row ending in a share bar. */
2825
- function UsageBreakdown(kit, { byType }) {
3077
+ /**
3078
+ * `trigger` is a free string on the wire, so a host that starts tagging
3079
+ * runs with something this dictionary has never heard of shows the tag
3080
+ * itself rather than a raw `trigger.<key>` lookup miss.
3081
+ */
3082
+ function triggerLabel(t, trigger) {
3083
+ const key = 'trigger.' + trigger
3084
+ const label = t(key)
3085
+ return label === key ? trigger : label
3086
+ }
3087
+
3088
+ /**
3089
+ * Spend across one bucket record, costliest first, each row ending in a
3090
+ * share bar. `label` turns a bucket key into its display name, which is a
3091
+ * dictionary lookup for the closed sets and the raw key for the open ones.
3092
+ */
3093
+ function UsageBreakdown(kit, { buckets, title, label }) {
2826
3094
  const { t } = kit
2827
- const rows = Object.entries(byType).sort((a, b) => b[1].usdKnown - a[1].usdKnown)
3095
+ const rows = Object.entries(buckets).sort((a, b) => b[1].usdKnown - a[1].usdKnown)
2828
3096
  if (rows.length === 0) return null
2829
3097
  const top = rows[0][1].usdKnown
2830
- return h('div', { className: 'tacit-usage-breakdown' },
2831
- h('div', { className: 'tacit-report-title' }, t('usage.breakdown')),
3098
+ return h('div', { key: title, className: 'tacit-usage-breakdown' },
3099
+ h('div', { className: 'tacit-report-title' }, t(title)),
2832
3100
  rows.map(([type, totals]) => {
2833
3101
  const share = top > 0 ? Math.round((totals.usdKnown / top) * 100) : 0
2834
3102
  return h('div', { key: type, className: 'tacit-breakdown-row' },
2835
- h('span', { className: 'tacit-breakdown-name' }, t('runtype.' + type)),
3103
+ h('span', { className: 'tacit-breakdown-name' }, label(type)),
2836
3104
  h('span', { className: 'tacit-breakdown-calls' }, fmtTokens(totals.billedCalls)),
2837
3105
  h('span', { className: 'tacit-breakdown-tokens' }, fmtTokens(tokensTotal(totals.tokens))),
2838
3106
  h('span', { className: 'tacit-breakdown-usd' }, usageSpend(kit, totals)),
@@ -2942,7 +3210,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2942
3210
  ? run.attempts.filter((attempt) => attempt !== null && typeof attempt === 'object')
2943
3211
  : null
2944
3212
  return h('div', { className: 'tacit-usage-attempts', id: 'tacit-run-' + runId, role: 'row' },
2945
- h('div', { className: 'tacit-usage-cell tacit-usage-attempts-cell', role: 'cell' },
3213
+ h('div', { className: 'tacit-usage-cell tacit-usage-attempts-cell', role: 'cell', 'aria-colspan': USAGE_COLUMNS.length },
2946
3214
  attempts === null
2947
3215
  ? h('p', { className: 'tacit-panel-hint' }, t('usage.loading'))
2948
3216
  : attempts.map((attempt) => AttemptRow(kit, attempt))))
@@ -2951,9 +3219,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2951
3219
  /** One run row; the first cell is the disclosure button for its attempts. */
2952
3220
  function RunRow(kit, { item, open, onToggleRun }) {
2953
3221
  const { t, fmtTime } = kit
3222
+ // A bootstrap run carries no workspace because it covers every session,
3223
+ // which is worth saying; any other run type missing the field is simply
3224
+ // a row with nothing to scope by.
3225
+ const unscoped = item.type === 'bootstrap' ? t('usage.scopeAll') : '—'
2954
3226
  const scope = item.workspace.length > 0 && item.turn !== null
2955
3227
  ? item.workspace + ' · #' + String(item.turn)
2956
- : (item.workspace.length > 0 ? item.workspace : (item.turn !== null ? '#' + String(item.turn) : '—'))
3228
+ : (item.workspace.length > 0 ? item.workspace : (item.turn !== null ? '#' + String(item.turn) : unscoped))
2957
3229
  const cell = (key, child) => h('span', { key, className: 'tacit-usage-cell', role: 'cell' }, child)
2958
3230
  return h('div', { className: 'tacit-usage-row', role: 'row' },
2959
3231
  cell('time', h('button', {
@@ -2968,7 +3240,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2968
3240
  cell('model', item.model.length > 0 ? item.model : '—'),
2969
3241
  cell('status', h('span', { className: 'tacit-chip tacit-status-' + item.status }, t('status.' + item.status))),
2970
3242
  cell('calls', fmtTokens(item.billedCalls)),
2971
- cell('tokens', fmtTokens(tokensTotal(item.tokens))),
3243
+ cell('tokens', h('span', { className: 'tacit-usage-tokens' },
3244
+ h('span', null, fmtTokens(tokensTotal(item.tokens))),
3245
+ h('span', { className: 'tacit-usage-split' }, fmtTokensSplit(item.tokens)))),
2972
3246
  cell('cost', usageSpend(kit, item)))
2973
3247
  }
2974
3248
 
@@ -3029,19 +3303,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3029
3303
  return h('div', { className: 'tacit-usage' },
3030
3304
  h('p', { className: 'tacit-panel-hint' }, t('usage.label')),
3031
3305
  h('div', { className: 'tacit-tiles' },
3032
- SpendTile(kit, 'today', t('usage.today'), usage.today),
3306
+ SpendTile(kit, 'today', t('usage.today'), usage.today,
3307
+ t('usage.autoToday', { n: String(usage.auto.today), budget: String(usage.auto.budget) })),
3033
3308
  SpendTile(kit, 'month', t('usage.month'), usage.month),
3034
3309
  SpendTile(kit, 'last30', t('usage.last30'), last30),
3035
3310
  SpendTile(kit, 'lifetime', t('usage.since', { day: since }), usage.lifetime)),
3036
3311
  h('div', { className: 'tacit-tiles' },
3037
3312
  UsageTile('calls', t('usage.calls'), fmtTokens(last30.billedCalls)),
3313
+ UsageTile('tokens', t('usage.tokens'), fmtTokensSplit(last30.tokens)),
3038
3314
  UsageTile('avg', t('usage.avgAnalysis'), last30.avgAnalysisUsd === null
3039
3315
  ? t('usage.priceUnavailable')
3040
3316
  : fmtUsd(last30.avgAnalysisUsd)),
3041
3317
  UsageTile('cached', t('usage.cachedRate'), fmtPct(last30.cachedInputRate)),
3042
- UsageTile('unpriced', t('usage.unpriced'), fmtTokens(last30.unpricedCalls))),
3318
+ UsageTile('unpriced', t('usage.unpriced'), fmtTokens(last30.unpricedCalls)),
3319
+ UsageTile('failedRepair', t('usage.failedRepair'), fmtUsd(last30.failedOrRepairUsd),
3320
+ t('usage.failedRepairSplit', { failed: fmtUsd(last30.failedUsd), repair: fmtUsd(last30.repairUsd) }))),
3043
3321
  BarStrip(kit, { series: which === '7' ? usage.series7 : usage.series30, which, onSeries: props.onSeries }),
3044
- UsageBreakdown(kit, { byType: usage.byType }),
3322
+ UsageBreakdown(kit, { buckets: usage.byType, title: 'usage.breakdown', label: (type) => t('runtype.' + type) }),
3323
+ UsageBreakdown(kit, { buckets: usage.byProvider, title: 'usage.byRoute', label: (provider) => provider }),
3324
+ UsageBreakdown(kit, { buckets: usage.byTrigger, title: 'usage.byTrigger', label: (trigger) => triggerLabel(t, trigger) }),
3045
3325
  WarnBar(kit, { id: 'daily', warning: usage.warnings.daily }),
3046
3326
  WarnBar(kit, { id: 'monthly', warning: usage.warnings.monthly }),
3047
3327
  UsageFilters(kit, {
@@ -3149,13 +3429,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3149
3429
 
3150
3430
  /**
3151
3431
  * The Pricing card body. Like `UsageCard` a plain renderer, not a
3152
- * component: it owns no hooks and no state. It builds nothing at all while
3153
- * collapsed the header summary already carries the headline rate, and
3154
- * this page re-renders on every 10s usage poll.
3432
+ * component: it owns no hooks and no state. It renders whether or not its
3433
+ * card is open, because the `SectionCard` around it hides a collapsed body
3434
+ * rather than unmounting it, which is what keeps find-in-page working over
3435
+ * the rate table.
3155
3436
  */
3156
- function PricingCard(kit, { pricing, open, onRefresh, refreshing }) {
3437
+ function PricingCard(kit, { pricing, onRefresh, refreshing }) {
3157
3438
  const { t, fmtTime } = kit
3158
- if (open !== true) return null
3159
3439
  const priced = pricingOf(pricing)
3160
3440
  const models = Object.keys(priced.models).sort()
3161
3441
  const when = priced.refreshedAt > 0
@@ -3224,6 +3504,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3224
3504
  + '.tacit-settings{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;margin-bottom:12px;display:flex;flex-direction:column;gap:8px}'
3225
3505
  + '.tacit-settings-title{font-size:12px;font-weight:600}.tacit-settings-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.tacit-settings-label{font-size:12px;color:var(--dsw-alias-label-secondary);min-width:150px}'
3226
3506
  + '.tacit-settings-notice{font-size:11px;color:var(--dsw-alias-state-success-primary)}'
3507
+ // The idle live region has to stay mounted to announce, but as an empty
3508
+ // flex item it still claims one of the panel's 10px gaps; -5px a side
3509
+ // gives that back.
3510
+ + '.tacit-panel>.tacit-settings-notice:empty{margin-block:-5px}'
3227
3511
  + '.tacit-select,.tacit-input{font:inherit;font-size:12px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:3px 8px}.tacit-input{width:90px}'
3228
3512
  + '.tacit-improve-btn{font:inherit;font-size:13px;font-weight:500;line-height:20px;color:var(--dsw-alias-label-secondary);background:transparent;border:0;border-radius:24px;cursor:pointer;height:28px;padding:0 8px;white-space:nowrap;display:inline-flex;align-items:center;gap:4px}.tacit-improve-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}'
3229
3513
  + '.tacit-check{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);flex:none;margin:2px 4px 0 0}'
@@ -3280,9 +3564,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3280
3564
  + '.tacit-usage-filters{display:flex;flex-direction:column;gap:6px}.tacit-filter-row{display:flex;flex-wrap:wrap;gap:8px}.tacit-filter{display:flex;flex-direction:column;gap:2px;min-width:0}.tacit-filter-label{font-size:10px;color:var(--dsw-alias-label-secondary)}'
3281
3565
  + '.tacit-usage-runs{display:flex;flex-direction:column;gap:6px}'
3282
3566
  + '.tacit-usage-table{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden}'
3283
- + '.tacit-usage-row{display:grid;grid-template-columns:minmax(90px,1fr) minmax(90px,1.2fr) minmax(80px,1.2fr) minmax(90px,1.2fr) 5rem 4rem 5rem 5rem;gap:6px;align-items:center;padding:5px 8px;border-top:1px solid var(--dsw-alias-border-l1);font-size:11px;font-variant-numeric:tabular-nums}'
3567
+ + '.tacit-usage-row{display:grid;grid-template-columns:minmax(90px,1fr) minmax(90px,1.2fr) minmax(80px,1.2fr) minmax(90px,1.2fr) 5rem 4rem 7rem 5rem;gap:6px;align-items:center;padding:5px 8px;border-top:1px solid var(--dsw-alias-border-l1);font-size:11px;font-variant-numeric:tabular-nums}'
3284
3568
  + '.tacit-usage-table>.tacit-usage-row:first-child{border-top:0}.tacit-usage-head{color:var(--dsw-alias-label-secondary);font-weight:600;background:var(--dsw-alias-bg-layer-2)}'
3285
3569
  + '.tacit-usage-cell{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'
3570
+ // The cell is nowrap, so the in/out split needs its own block to sit under the total.
3571
+ + '.tacit-usage-tokens{display:flex;flex-direction:column}.tacit-usage-split{font-size:10px;color:var(--dsw-alias-label-secondary)}'
3286
3572
  + '.tacit-run-toggle{width:100%;text-align:left}'
3287
3573
  + '.tacit-usage-attempts{border-top:1px dashed var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);padding:6px 8px}.tacit-usage-attempts-cell{display:flex;flex-direction:column;gap:4px}'
3288
3574
  + '.tacit-attempt{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-primary)}.tacit-attempt-meta,.tacit-attempt-tokens{color:var(--dsw-alias-label-secondary)}.tacit-attempt-usd{margin-left:auto;font-variant-numeric:tabular-nums}'
@@ -3393,11 +3679,18 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3393
3679
  refreshPricing,
3394
3680
  openConfirm,
3395
3681
  closeConfirm,
3682
+ confirmAction,
3396
3683
  fetchBootstrapPreview,
3684
+ fetchReceipt,
3685
+ storeFor,
3686
+ analyzeTurn,
3687
+ coachSelected,
3397
3688
  clearAllRoot,
3398
3689
  clearUsageHistory,
3399
3690
  runNotice,
3400
3691
  fmtRate,
3692
+ fmtTokensSplit,
3693
+ ConfirmDialog,
3401
3694
  UsageCard,
3402
3695
  PricingCard,
3403
3696
  },