dsh-tacit 0.4.0 → 0.6.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
@@ -53,13 +53,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
53
53
  'status.trendCorrections': '你纠正智能体:{a} → {b}',
54
54
  'status.trendMessy': '混乱轮次:{a} → {b}',
55
55
  'status.trendTokens': '每轮 tokens:{a} → {b}',
56
- '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',
57
58
  'turn.enrichment': '发送前补充的上下文',
58
59
  'trigger.auto': '自动',
59
60
  'trigger.correction': '纠正',
60
61
  'trigger.manual': '手动',
61
62
  'trigger.bootstrap': '引导',
62
63
  'trigger.good': '做对了',
64
+ 'trigger.feedback': '反馈',
65
+ 'trigger.send': '发送',
63
66
  'report.strengths': '这次做对了什么',
64
67
  'report.lesson': '经验',
65
68
  'settings.learnGood': '也从混乱之后的顺利提示词中学习',
@@ -89,6 +92,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
89
92
  'steer.workspace': '仅 {name}',
90
93
  'steer.scope': '适用范围',
91
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.receiptTurns': '{n} 轮',
112
+ 'steer.receiptTrialNote': '始于 {started} · 基线:杂乱 {messy}、纠正 {corrected}',
113
+ 'steer.receiptCalls': '{n} 次调用',
114
+ 'steer.enabled': '启用',
115
+ 'steer.options': '选项',
116
+ 'steer.receiptTriggers': '触发类别',
117
+ 'steer.receiptEvidence': '证据',
118
+ 'steer.receiptEvidenceValue': '{turns} 轮,来自 {conversations} 个会话',
119
+ 'steer.receiptCost': '提炼花费',
120
+ 'steer.moveTo': '移到工作区',
121
+ 'steer.notSeen': '自 {date} 起未见',
92
122
  'settings.auto': '自动分析混乱轮次',
93
123
  'settings.budget': '每日自动分析上限',
94
124
  settings: '设置',
@@ -167,10 +197,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
167
197
  'usage.last30': '最近 30 天',
168
198
  'usage.since': '自 {day} 起',
169
199
  'usage.calls': '计费调用(30 天)',
200
+ 'usage.tokens': 'Tokens(30 天)',
170
201
  'usage.avgAnalysis': '单次分析中位成本',
171
202
  'usage.cachedRate': '输入缓存命中率(30 天)',
172
203
  'usage.unpriced': '无价目调用(30 天)',
173
204
  'usage.unpricedShort': '{n} 次无价目',
205
+ 'usage.failedRepair': '失败或修复调用花费(30 天)',
206
+ 'usage.failedRepairSplit': '失败 {failed} · 修复 {repair}',
207
+ 'usage.autoToday': '今日自动分析 {n} / {budget}',
174
208
  'usage.priceUnavailable': '无价目',
175
209
  'usage.chartLabel': '每日花费,最近 {n} 天',
176
210
  'usage.chartSummary': '最近 {n} 天:合计 {total};最高 {day} {max}',
@@ -178,6 +212,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
178
212
  'usage.series7': '7 天',
179
213
  'usage.series30': '30 天',
180
214
  'usage.breakdown': '按操作分类(最近 30 天)',
215
+ 'usage.byRoute': '按线路分类(最近 30 天)',
216
+ 'usage.byTrigger': '按触发方式分类(最近 30 天)',
181
217
  'usage.filters': '筛选',
182
218
  'usage.loading': '正在载入这次运行的调用明细…',
183
219
  'usage.page': '第 {page} / {pages} 页',
@@ -292,6 +328,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
292
328
  'err.network': '无法连接 Tacit 服务。',
293
329
  'err.internal': '内部错误:{detail}',
294
330
  'err.continuation': '这是一条纯粹的「继续」类消息(没有可分析的意图)——请分析它之前的那一轮。',
331
+ 'err.directive-policy': '指令不能改变工具权限、审批或沙箱设置,请换一种说法。',
332
+ 'err.already-analyzed': '这一轮已有报告;点击「重新分析」可再花一次费用分析。',
333
+ 'err.unknown-directive': '找不到这条指令。',
295
334
  }
296
335
 
297
336
  const DICT_EN = {
@@ -303,13 +342,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
303
342
  'status.trendCorrections': 'You corrected the agent: {a} → {b}',
304
343
  'status.trendMessy': 'Messy turns: {a} → {b}',
305
344
  'status.trendTokens': 'Tokens/turn: {a} → {b}',
306
- 'status.trendHint': '(first {n} turns vs. latest {n}, measured)',
345
+ 'status.trendHint': '(first {n} turns vs. latest {n}, measured — before/after on your own turns, not a controlled comparison)',
346
+ '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',
307
347
  'turn.enrichment': 'Context added before the send',
308
348
  'trigger.auto': 'auto',
309
349
  'trigger.correction': 'correction',
310
350
  'trigger.manual': 'manual',
311
351
  'trigger.bootstrap': 'bootstrap',
312
352
  'trigger.good': 'what worked',
353
+ 'trigger.feedback': 'feedback',
354
+ 'trigger.send': 'send',
313
355
  'report.strengths': 'What this prompt got right',
314
356
  'report.lesson': 'Lesson',
315
357
  'settings.learnGood': 'Also learn from a clean prompt right after a messy turn',
@@ -339,6 +381,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
339
381
  'steer.workspace': 'only {name}',
340
382
  'steer.scope': 'Applies to',
341
383
  'steer.everywhere': 'Everywhere',
384
+ 'steer.receipt': 'Where this directive came from',
385
+ 'steer.review': 'Review new directives before their trial',
386
+ 'steer.startTrial': 'Start trial',
387
+ 'steer.receiptLoading': 'Reading…',
388
+ 'steer.receiptCopy': 'Copy as JSON',
389
+ 'steer.receiptNever': 'never',
390
+ 'steer.receiptNoRun': 'no ledger run',
391
+ 'steer.receiptId': 'Id',
392
+ 'steer.receiptScope': 'Applies to',
393
+ 'steer.receiptStatus': 'Status',
394
+ 'steer.receiptSource': 'Source',
395
+ 'steer.receiptCreated': 'Created',
396
+ 'steer.receiptUpdated': 'Updated',
397
+ 'steer.receiptEvaluated': 'Evaluated',
398
+ 'steer.receiptVersion': 'Version',
399
+ 'steer.receiptTrial': 'Trial window',
400
+ 'steer.receiptTurns': '{n} turns',
401
+ 'steer.receiptTrialNote': 'from {started} · baseline messy {messy}, corrections {corrected}',
402
+ 'steer.receiptCalls': '{n} calls',
403
+ 'steer.enabled': 'Enabled',
404
+ 'steer.options': 'Options',
405
+ 'steer.receiptTriggers': 'Trigger categories',
406
+ 'steer.receiptEvidence': 'Evidence',
407
+ 'steer.receiptEvidenceValue': '{turns} turns across {conversations} conversations',
408
+ 'steer.receiptCost': 'Distillation cost',
409
+ 'steer.moveTo': 'Move to workspace',
410
+ 'steer.notSeen': 'not seen since {date}',
342
411
  'settings.auto': 'Auto-analyze messy turns',
343
412
  'settings.budget': 'Daily cap on automatic analyses',
344
413
  settings: 'Settings',
@@ -417,10 +486,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
417
486
  'usage.last30': 'Last 30 days',
418
487
  'usage.since': 'Since {day}',
419
488
  'usage.calls': 'Billed calls (30 d)',
489
+ 'usage.tokens': 'Tokens (30 d)',
420
490
  'usage.avgAnalysis': 'Median cost per analysis',
421
491
  'usage.cachedRate': 'Cached input rate (30 d)',
422
492
  'usage.unpriced': 'Unpriced calls (30 d)',
423
493
  'usage.unpricedShort': '{n} unpriced',
494
+ 'usage.failedRepair': 'Spent on failed or repair calls (30 d)',
495
+ 'usage.failedRepairSplit': 'failed {failed} · repair {repair}',
496
+ 'usage.autoToday': 'automatic analyses today {n} / {budget}',
424
497
  'usage.priceUnavailable': 'No price',
425
498
  'usage.chartLabel': 'Daily spend, last {n} days',
426
499
  'usage.chartSummary': 'Last {n} days: total {total}; highest {day} {max}',
@@ -428,6 +501,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
428
501
  'usage.series7': '7 d',
429
502
  'usage.series30': '30 d',
430
503
  'usage.breakdown': 'By operation (last 30 days)',
504
+ 'usage.byRoute': 'By route (last 30 days)',
505
+ 'usage.byTrigger': 'By trigger (last 30 days)',
431
506
  'usage.filters': 'Filters',
432
507
  'usage.loading': 'Loading this run\u2019s calls…',
433
508
  'usage.page': 'Page {page} of {pages}',
@@ -542,6 +617,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
542
617
  'err.network': 'Could not reach the Tacit service.',
543
618
  'err.internal': 'Internal error: {detail}',
544
619
  'err.continuation': 'This is a bare continuation ("continue", "go ahead") with nothing to analyze — analyze the turn before it instead.',
620
+ 'err.directive-policy': 'A directive cannot change tool permissions, approvals or the sandbox. Reword it.',
621
+ 'err.already-analyzed': 'This turn already has a report; Re-analyze pays for a fresh one.',
622
+ 'err.unknown-directive': 'No directive with that id.',
545
623
  }
546
624
 
547
625
  // ── API client (the host half's own routes on the harness origin) ──────
@@ -641,6 +719,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
641
719
  auto: null,
642
720
  steering: null, // {enabled, text}
643
721
  workspaces: [], // [{cwd, label}] from /state
722
+ receipts: {}, // directive id → the /directive-receipt receipt, re-read on each expand
644
723
  trend: null, // measured early-vs-recent trend
645
724
  bootstrap: null, // {running, done, total}
646
725
  coached: [], // cross-session coached-prompt entries
@@ -1029,13 +1108,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1029
1108
  function closeConfirm() {
1030
1109
  rootStore.confirm = null
1031
1110
  notifyRoot()
1111
+ restoreConfirmOpener()
1032
1112
  }
1033
1113
 
1034
1114
  /** Run the parked destructive action, whichever it is, and close the dialog. */
1035
1115
  async function confirmAction() {
1036
1116
  const kind = rootStore.confirm !== null && typeof rootStore.confirm === 'object' ? rootStore.confirm.kind : null
1037
- rootStore.confirm = null
1038
- notifyRoot()
1117
+ closeConfirm()
1039
1118
  if (kind === 'reports') await clearAllRoot()
1040
1119
  else if (kind === 'usage') await clearUsageHistory()
1041
1120
  }
@@ -1151,6 +1230,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1151
1230
  notifyRoot()
1152
1231
  }
1153
1232
 
1233
+ /** One directive's audit receipt. Re-read on every expand: a trial's counters move while it runs. */
1234
+ async function fetchReceipt(id) {
1235
+ const key = String(id)
1236
+ if (key.length === 0) return
1237
+ rootStore.error = null
1238
+ try {
1239
+ const result = await api('/directive-receipt', { id: key })
1240
+ if (result !== null && typeof result === 'object' && result.ok === true
1241
+ && result.receipt !== null && typeof result.receipt === 'object') {
1242
+ rootStore.receipts[key] = result.receipt
1243
+ } else {
1244
+ rootStore.error = { code: result !== null && typeof result === 'object' && typeof result.code === 'string' ? result.code : 'bad-request', detail: '' }
1245
+ }
1246
+ } catch (error) {
1247
+ rootStore.error = errorOf(error)
1248
+ }
1249
+ notifyRoot()
1250
+ }
1251
+
1154
1252
  async function initStore(store) {
1155
1253
  if (store === null || store.initStarted) return
1156
1254
  store.initStarted = true
@@ -1181,7 +1279,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1181
1279
  store.error = null
1182
1280
  store.notice = null
1183
1281
  notify(store)
1184
- api('/analyze', { sessionId: store.sessionId, turn })
1282
+ // A turn that already has a report is only re-analyzed on purpose; the button reads "Re-analyze" then.
1283
+ api('/analyze', { sessionId: store.sessionId, turn, ...(store.reports[String(turn)] ? { force: true } : {}) })
1185
1284
  .then((result) => {
1186
1285
  if (result !== null && typeof result === 'object' && result.ok && result.report !== null && typeof result.report === 'object') {
1187
1286
  store.reports[String(turn)] = result.report
@@ -1245,7 +1344,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1245
1344
  // `busy` means an analysis of that turn was already running — a
1246
1345
  // race with the auto-analyzer, not a failure of this batch.
1247
1346
  const code = typeof entry.code === 'string' && entry.code.length > 0 ? entry.code : 'call-failed'
1248
- if (failure === null && code !== 'busy') failure = { code, detail: '' }
1347
+ if (failure === null && code !== 'busy' && code !== 'already-analyzed') failure = { code, detail: '' }
1249
1348
  }
1250
1349
  if (failure !== null) store.error = failure
1251
1350
  if (result.profile !== null && typeof result.profile === 'object') store.profile = result.profile
@@ -1575,14 +1674,30 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1575
1674
  }
1576
1675
 
1577
1676
  /**
1578
- * Billed tokens in one bucket set: uncached input + output + cache reads +
1579
- * cache writes. `reasoningTokens` is a subset of `outputTokens`, so adding
1580
- * it would count every reasoning token twice.
1677
+ * Billed tokens in one bucket set, split the way they are priced:
1678
+ * everything the prompt cost (uncached input, cache reads, cache writes)
1679
+ * against what came back. `reasoningTokens` is a subset of `outputTokens`,
1680
+ * so adding it would count every reasoning token twice.
1581
1681
  */
1582
- function tokensTotal(buckets) {
1583
- if (buckets === null || typeof buckets !== 'object') return 0
1682
+ function tokensInOut(buckets) {
1683
+ if (buckets === null || typeof buckets !== 'object') return { input: 0, output: 0 }
1584
1684
  const num = (value) => (typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0)
1585
- return num(buckets.inputTokens) + num(buckets.outputTokens) + num(buckets.cacheReadTokens) + num(buckets.cacheWriteTokens)
1685
+ return {
1686
+ input: num(buckets.inputTokens) + num(buckets.cacheReadTokens) + num(buckets.cacheWriteTokens),
1687
+ output: num(buckets.outputTokens),
1688
+ }
1689
+ }
1690
+
1691
+ /** Both halves of `tokensInOut` at once. */
1692
+ function tokensTotal(buckets) {
1693
+ const split = tokensInOut(buckets)
1694
+ return split.input + split.output
1695
+ }
1696
+
1697
+ /** `tokensInOut` as the one line the tiles and the run rows both show. */
1698
+ function fmtTokensSplit(buckets) {
1699
+ const split = tokensInOut(buckets)
1700
+ return 'in ' + fmtTokens(split.input) + ' · out ' + fmtTokens(split.output)
1586
1701
  }
1587
1702
 
1588
1703
  function usageNum(value) {
@@ -1600,7 +1715,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1600
1715
  }
1601
1716
  }
1602
1717
 
1603
- /** One period card, zero-filled; the two derived figures stay nullable. */
1718
+ /**
1719
+ * One period card, zero-filled; the two averages stay nullable.
1720
+ * `failedUsd` and `repairUsd` overlap on a failed repair, so they can sum
1721
+ * past `failedOrRepairUsd`, which counts that union once.
1722
+ */
1604
1723
  function usagePeriod(value) {
1605
1724
  const source = value !== null && typeof value === 'object' ? value : {}
1606
1725
  return {
@@ -1608,8 +1727,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1608
1727
  billedCalls: usageNum(source.billedCalls),
1609
1728
  unmeteredCalls: usageNum(source.unmeteredCalls),
1610
1729
  unpricedCalls: usageNum(source.unpricedCalls),
1730
+ failedCalls: usageNum(source.failedCalls),
1611
1731
  tokens: usageTokens(source.tokens),
1612
1732
  usdKnown: usageNum(source.usdKnown),
1733
+ failedUsd: usageNum(source.failedUsd),
1734
+ repairUsd: usageNum(source.repairUsd),
1735
+ failedOrRepairUsd: usageNum(source.failedOrRepairUsd),
1613
1736
  avgAnalysisUsd: typeof source.avgAnalysisUsd === 'number' && Number.isFinite(source.avgAnalysisUsd) ? source.avgAnalysisUsd : null,
1614
1737
  cachedInputRate: typeof source.cachedInputRate === 'number' && Number.isFinite(source.cachedInputRate) ? source.cachedInputRate : null,
1615
1738
  }
@@ -1646,8 +1769,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1646
1769
  billedCalls: usageNum(source.billedCalls),
1647
1770
  unmeteredCalls: usageNum(source.unmeteredCalls),
1648
1771
  unpricedCalls: usageNum(source.unpricedCalls),
1772
+ failedCalls: usageNum(source.failedCalls),
1649
1773
  tokens: usageTokens(source.tokens),
1650
1774
  usdKnown: usageNum(source.usdKnown),
1775
+ failedUsd: usageNum(source.failedUsd),
1651
1776
  trigger: text('trigger'),
1652
1777
  startedAt: usageNum(source.startedAt),
1653
1778
  endedAt: usageNum(source.endedAt),
@@ -1668,6 +1793,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1668
1793
  if (value === null || typeof value !== 'object' || value.ok !== true) return null
1669
1794
  const runs = value.runs !== null && typeof value.runs === 'object' ? value.runs : {}
1670
1795
  const warnings = value.warnings !== null && typeof value.warnings === 'object' ? value.warnings : {}
1796
+ const auto = value.auto !== null && typeof value.auto === 'object' ? value.auto : {}
1671
1797
  return {
1672
1798
  trackingSince: usageNum(value.trackingSince),
1673
1799
  pricing: value.pricing !== null && typeof value.pricing === 'object' ? value.pricing : {},
@@ -1678,9 +1804,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1678
1804
  lifetime: usagePeriod(value.lifetime),
1679
1805
  byType: usageBuckets(value.byType),
1680
1806
  byModel: usageBuckets(value.byModel),
1807
+ byProvider: usageBuckets(value.byProvider),
1808
+ byTrigger: usageBuckets(value.byTrigger),
1681
1809
  series7: usageSeries(value.series7),
1682
1810
  series30: usageSeries(value.series30),
1683
1811
  warnings: { daily: usageWarning(warnings.daily), monthly: usageWarning(warnings.monthly) },
1812
+ auto: { today: usageNum(auto.today), budget: usageNum(auto.budget) },
1684
1813
  runs: {
1685
1814
  items: (Array.isArray(runs.items) ? runs.items : []).map(usageRunItem),
1686
1815
  page: usageNum(runs.page) > 0 ? Math.floor(usageNum(runs.page)) : 1,
@@ -1755,10 +1884,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1755
1884
  return h('div', { className: 'tacit-progress' },
1756
1885
  h('div', { className: 'tacit-progress-head' },
1757
1886
  h('span', { className: 'tacit-progress-title' }, t('status.learned', { count: String(analyzed) })),
1758
- h('span', { className: 'tacit-progress-count' }, autoOn ? t('status.autoOn', { today: String(today), budget: String(budget) }) : '')),
1887
+ // The same badge idiom the directive cards use: a dot plus a label,
1888
+ // tinted by state. Off is said in the line below, not twice.
1889
+ autoOn
1890
+ ? h('span', { className: 'tacit-badge tacit-badge-active' },
1891
+ h('span', { className: 'tacit-badge-dot', 'aria-hidden': 'true' }),
1892
+ t('status.autoOn', { today: String(today), budget: String(budget) }))
1893
+ : null),
1759
1894
  h('div', { className: 'tacit-progress-text' }, autoOn ? t('status.autoHint') : t('status.autoOff')),
1760
1895
  hasTrend
1761
- ? h('div', { className: 'tacit-trend' },
1896
+ ? h('div', { className: 'tacit-trend', title: t('status.trendDocs') },
1762
1897
  h('span', { className: 'tacit-chip' }, t('status.trendCorrections', { a: pct(trend.early.correctionRate), b: pct(trend.recent.correctionRate) })),
1763
1898
  h('span', { className: 'tacit-chip' }, t('status.trendMessy', { a: pct(trend.early.messyRate), b: pct(trend.recent.messyRate) })),
1764
1899
  h('span', { className: 'tacit-chip' }, t('status.trendTokens', { a: fmt(trend.early.tokensPerTurn), b: fmt(trend.recent.tokensPerTurn) })),
@@ -1854,8 +1989,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1854
1989
  const status = turn.finished === true
1855
1990
  ? null
1856
1991
  : (isNewest === true
1857
- ? h('span', { className: 'tacit-row-live' }, t('turn.running'))
1858
- : h('span', { className: 'tacit-chip tacit-chip-muted' }, t('turn.interrupted')))
1992
+ ? h('span', { className: 'tacit-badge tacit-badge-active' },
1993
+ h('span', { className: 'tacit-badge-dot', 'aria-hidden': 'true' }),
1994
+ t('turn.running'))
1995
+ : h('span', { className: 'tacit-badge tacit-badge-muted' }, t('turn.interrupted')))
1859
1996
 
1860
1997
  return h('div', { className: 'tacit-row' + (report !== null ? ' tacit-row-analyzed' : '') },
1861
1998
  h('div', { className: 'tacit-row-head' },
@@ -1873,35 +2010,47 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1873
2010
  h('span', { className: 'tacit-row-time' }, fmtTime(turn.startedAt)),
1874
2011
  report !== null ? h('span', { className: 'tacit-chip tacit-chip-trigger' }, t('trigger.' + report.trigger)) : null,
1875
2012
  status),
1876
- h('div', { className: 'tacit-row-chips' },
1877
- h('span', { className: 'tacit-chip' }, t('turn.tools', { n: String(tools) })),
1878
- h('span', { className: 'tacit-chip' }, t('turn.steps', { n: String(typeof turn.steps === 'number' ? turn.steps : 0) })),
1879
- h('span', { className: 'tacit-chip' }, t('turn.tokens', { n: fmt(tokens) })),
1880
- typeof turn.retries === 'number' && turn.retries > 0
1881
- ? h('span', { className: 'tacit-chip tacit-chip-warn' }, t('turn.retries', { n: String(turn.retries) }))
1882
- : null,
2013
+ // Two controls only, and the row's own action is the solid one:
2014
+ // opening the report is a disclosure, so it stays quiet.
2015
+ h('div', { className: 'tacit-row-actions' },
1883
2016
  report !== null
1884
- ? h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => toggleReport(store, turn.turn) },
1885
- t('turn.report') + (reportOpen ? ' ▾' : ' ▸'))
2017
+ ? h('button', {
2018
+ type: 'button',
2019
+ className: 'tacit-btn tacit-btn-sm tacit-btn-quiet',
2020
+ 'aria-expanded': reportOpen,
2021
+ onClick: () => toggleReport(store, turn.turn),
2022
+ }, t('turn.report'),
2023
+ h('span', { className: 'tacit-chevron' + (reportOpen ? ' tacit-chevron-open' : ''), 'aria-hidden': 'true' }, '›'))
1886
2024
  : null,
1887
2025
  h('button', {
1888
2026
  type: 'button',
1889
- className: 'tacit-btn tacit-btn-sm tacit-btn-quiet',
2027
+ className: 'tacit-btn tacit-btn-sm',
1890
2028
  disabled: analyzing,
1891
2029
  onClick: () => analyzeTurn(store, turn.turn),
1892
2030
  }, analyzing ? t('turn.analyzing') : (report !== null ? t('turn.reanalyze') : t('turn.analyze'))))),
1893
2031
  prompt.length > 0
1894
2032
  ? h('div', { className: 'tacit-row-prompt' },
1895
- h('button', {
1896
- type: 'button',
1897
- className: 'tacit-row-prompt-text',
1898
- onClick: () => setExpanded((open) => !open),
1899
- }, expanded ? prompt : promptPreview),
2033
+ // A long prompt is one control: the text itself toggles, and the
2034
+ // trailing word says so. A short one is plain text.
1900
2035
  prompt.length > 120
1901
- ? h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => setExpanded((open) => !open) },
1902
- expanded ? t('turn.collapse') : t('turn.expand'))
1903
- : null)
2036
+ ? h('button', {
2037
+ type: 'button',
2038
+ className: 'tacit-row-prompt-text',
2039
+ 'aria-expanded': expanded,
2040
+ onClick: () => setExpanded((open) => !open),
2041
+ }, expanded ? prompt : promptPreview,
2042
+ h('span', { className: 'tacit-row-more' }, expanded ? t('turn.collapse') : t('turn.expand')))
2043
+ : h('div', { className: 'tacit-row-prompt-text tacit-row-prompt-static' }, prompt))
1904
2044
  : null,
2045
+ // The measurements are context, not headline: one quiet dot-separated
2046
+ // line under the prompt rather than four bordered chips in the header.
2047
+ h('div', { className: 'tacit-row-meta' },
2048
+ h('span', null, t('turn.tools', { n: String(tools) })),
2049
+ h('span', null, t('turn.steps', { n: String(typeof turn.steps === 'number' ? turn.steps : 0) })),
2050
+ h('span', null, t('turn.tokens', { n: fmt(tokens) })),
2051
+ typeof turn.retries === 'number' && turn.retries > 0
2052
+ ? h('span', { className: 'tacit-row-meta-warn' }, t('turn.retries', { n: String(turn.retries) }))
2053
+ : null),
1905
2054
  typeof turn.enrichment === 'string' && turn.enrichment.length > 0
1906
2055
  ? h('div', { className: 'tacit-row-enrichment' },
1907
2056
  h('span', { className: 'tacit-report-title' }, t('turn.enrichment')),
@@ -1965,35 +2114,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1965
2114
 
1966
2115
  return h('div', { className: 'tacit-settings' },
1967
2116
  h('div', { className: 'tacit-settings-title' }, t('settings.title')),
1968
- h('div', { className: 'tacit-settings-row' },
1969
- h('label', { className: 'tacit-settings-label' }, t('settings.model')),
1970
- h('select', {
1971
- className: 'tacit-select',
1972
- value: model,
1973
- onChange: (event) => applyModel(event.target.value),
1974
- },
1975
- h('option', { value: 'deepseek-v4-flash' }, 'deepseek-v4-flash'),
1976
- h('option', { value: 'deepseek-v4-pro' }, 'deepseek-v4-pro'))),
1977
- h('div', { className: 'tacit-settings-row' },
1978
- h('label', { className: 'tacit-settings-label' }, t('settings.auto')),
1979
- h('input', { type: 'checkbox', checked: auto, onChange: toggleAuto })),
1980
- h('div', { className: 'tacit-settings-row' },
1981
- h('label', { className: 'tacit-settings-label' }, t('settings.learnGood')),
1982
- h('input', { type: 'checkbox', checked: learnGood, onChange: toggleLearnGood })),
1983
- h('div', { className: 'tacit-settings-row' },
1984
- h('label', { className: 'tacit-settings-label' }, t('settings.budget')),
2117
+ FieldRow(t('settings.model'), h('select', {
2118
+ id: 'tacit-tab-model',
2119
+ className: 'tacit-select',
2120
+ value: model,
2121
+ onChange: (event) => applyModel(event.target.value),
2122
+ },
2123
+ h('option', { value: 'deepseek-v4-flash' }, 'deepseek-v4-flash'),
2124
+ h('option', { value: 'deepseek-v4-pro' }, 'deepseek-v4-pro')), 'tacit-tab-model'),
2125
+ FieldRow(t('settings.budget'), [
1985
2126
  h('input', {
2127
+ key: 'input',
2128
+ id: 'tacit-tab-budget',
1986
2129
  className: 'tacit-input',
1987
2130
  type: 'number',
1988
2131
  min: 0,
1989
2132
  value: budgetText,
1990
2133
  onChange: (event) => setBudgetText(event.target.value),
1991
2134
  }),
1992
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: applyBudget }, t('settings.apply'))),
1993
- h('div', { className: 'tacit-settings-row' },
1994
- h('label', { className: 'tacit-settings-label' }, t('settings.live')),
1995
- h('input', { type: 'checkbox', checked: live, onChange: toggleLive })),
1996
- h('div', { className: 'tacit-settings-row' },
2135
+ h('button', { key: 'apply', type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: applyBudget }, t('settings.apply')),
2136
+ ], 'tacit-tab-budget'),
2137
+ // The three switches read as one group of clickable sentences, the way
2138
+ // the Agent guidance options do.
2139
+ h('div', { className: 'tacit-options' },
2140
+ OptionRow(t('settings.auto'), auto, toggleAuto),
2141
+ OptionRow(t('settings.learnGood'), learnGood, toggleLearnGood),
2142
+ OptionRow(t('settings.live'), live, toggleLive)),
2143
+ h('div', { className: 'tacit-inline' },
1997
2144
  h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: onClear }, t('settings.clear')),
1998
2145
  notice !== null ? h('span', { className: 'tacit-settings-notice' }, notice) : null))
1999
2146
  }
@@ -2025,7 +2172,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2025
2172
  h('div', { className: 'tacit-head-title' }, t('tab')),
2026
2173
  h('button', {
2027
2174
  type: 'button',
2028
- className: 'tacit-btn',
2175
+ className: 'tacit-btn tacit-btn-sm tacit-btn-quiet',
2029
2176
  onClick: () => setSettingsOpen((open) => !open),
2030
2177
  }, settingsOpen ? t('settings.close') : t('settings'))),
2031
2178
  StatusCard(kit, { config, profile, auto: store.auto }),
@@ -2045,11 +2192,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2045
2192
  ? h('div', { className: 'tacit-empty' }, t('empty'))
2046
2193
  : h('div', null,
2047
2194
  h('div', { className: 'tacit-toolbar' },
2048
- h('span', { className: 'tacit-panel-hint tacit-toolbar-hint' }, t('manual.hint')),
2049
2195
  BootstrapButton(kit, { bootstrap: store.bootstrap, onClick: () => bootstrapSession(store) }),
2050
2196
  h('button', {
2051
2197
  type: 'button',
2052
- className: 'tacit-btn tacit-btn-sm',
2198
+ className: 'tacit-btn tacit-btn-sm' + (store.selecting ? '' : ' tacit-btn-quiet'),
2053
2199
  onClick: () => toggleSelecting(store),
2054
2200
  }, store.selecting ? t('coach.selectDone') : t('coach.select')),
2055
2201
  store.selecting
@@ -2060,6 +2206,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2060
2206
  onClick: () => coachSelected(store),
2061
2207
  }, store.batchRunning ? t('coach.batchRunning') : t('coach.selected', { n: String(selectedCount) }))
2062
2208
  : null),
2209
+ h('div', { className: 'tacit-toolbar-note' }, t('manual.hint')),
2063
2210
  turns.map((turn, index) => h(TurnRow, { key: String(turn.turn), kit, store, turn, isNewest: index === 0 }))))
2064
2211
  }
2065
2212
  }
@@ -2119,21 +2266,23 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2119
2266
  : preview.error !== null
2120
2267
  ? h('div', { className: 'tacit-error' },
2121
2268
  t('err.' + String(preview.error.code), { detail: String(preview.error.detail || '') }))
2122
- : h('div', null,
2269
+ // What changed is the one sentence that decides Apply, so it
2270
+ // reads before the two columns rather than under them.
2271
+ : h('div', { className: 'tacit-modal-body' },
2272
+ preview.data !== null && typeof preview.data === 'object' && typeof preview.data.rationale === 'string' && preview.data.rationale.length > 0
2273
+ ? h('div', { className: 'tacit-modal-rationale' },
2274
+ h('div', { className: 'tacit-modal-col-title' }, t('preview.rationale')),
2275
+ h('div', { className: 'tacit-modal-rationale-text' }, preview.data.rationale))
2276
+ : null,
2123
2277
  unchanged
2124
2278
  ? h('div', { className: 'tacit-modal-unchanged' }, t('preview.unchanged'))
2125
2279
  : h('div', { className: 'tacit-modal-cols' },
2126
2280
  h('div', { className: 'tacit-modal-col' },
2127
2281
  h('div', { className: 'tacit-modal-col-title' }, t('preview.original')),
2128
2282
  h('pre', { className: 'tacit-pre' }, preview.original)),
2129
- h('div', { className: 'tacit-modal-col' },
2283
+ h('div', { className: 'tacit-modal-col tacit-modal-col-improved' },
2130
2284
  h('div', { className: 'tacit-modal-col-title' }, t('preview.improved')),
2131
2285
  h('pre', { className: 'tacit-pre' }, improvedText))),
2132
- preview.data !== null && typeof preview.data === 'object' && typeof preview.data.rationale === 'string' && preview.data.rationale.length > 0
2133
- ? h('div', { className: 'tacit-modal-rationale' },
2134
- h('div', { className: 'tacit-modal-col-title' }, t('preview.rationale')),
2135
- h('div', { className: 'tacit-modal-rationale-text' }, preview.data.rationale))
2136
- : null,
2137
2286
  h('div', { className: 'tacit-modal-actions' },
2138
2287
  unchanged
2139
2288
  ? null
@@ -2214,6 +2363,37 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2214
2363
 
2215
2364
  // ── Collapsible section card (Settings → Tacit) ────────────────────────
2216
2365
 
2366
+ /**
2367
+ * One settings row: a label that takes the slack and wraps, and its
2368
+ * control(s) right-aligned. `htmlFor` names the control when it has an id;
2369
+ * checkbox settings use `OptionRow` instead, which is a label all through.
2370
+ */
2371
+ function FieldRow(label, control, htmlFor) {
2372
+ const controls = Array.isArray(control) ? control : [control]
2373
+ return h('div', { className: 'tacit-field' },
2374
+ h('label', {
2375
+ className: 'tacit-field-label',
2376
+ ...(typeof htmlFor === 'string' && htmlFor.length > 0 ? { htmlFor } : {}),
2377
+ }, label),
2378
+ h('div', { className: 'tacit-field-control' }, ...controls))
2379
+ }
2380
+
2381
+ /**
2382
+ * One checkbox setting as a clickable `<label>`, so the hit target is the
2383
+ * whole sentence rather than a 14px box.
2384
+ */
2385
+ function OptionRow(label, checked, onChange, extra) {
2386
+ return h('label', { className: 'tacit-option' },
2387
+ h('input', {
2388
+ type: 'checkbox',
2389
+ className: 'tacit-check',
2390
+ checked,
2391
+ onChange,
2392
+ ...(extra !== null && typeof extra === 'object' ? extra : {}),
2393
+ }),
2394
+ h('span', null, label))
2395
+ }
2396
+
2217
2397
  /**
2218
2398
  * One accessible collapsible card.
2219
2399
  *
@@ -2243,10 +2423,15 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2243
2423
  typeof summary === 'string' && summary.length > 0
2244
2424
  ? h('span', { className: 'tacit-card-summary' }, summary)
2245
2425
  : null,
2246
- count !== undefined && count !== null
2426
+ // A zero count is silence, not a figure worth a chip.
2427
+ typeof count === 'number' && count > 0
2247
2428
  ? h('span', { className: 'tacit-card-count' }, String(count))
2248
2429
  : null,
2249
- h('span', { className: 'tacit-card-chevron', 'aria-label': label, title: label }, isOpen ? '▾' : '▸')),
2430
+ h('span', {
2431
+ className: 'tacit-chevron tacit-card-chevron' + (isOpen ? ' tacit-chevron-open' : ''),
2432
+ 'aria-label': label,
2433
+ title: label,
2434
+ }, '›')),
2250
2435
  h('div', { className: 'tacit-card-body', id: bodyId, hidden: !isOpen }, ...kids))
2251
2436
  }
2252
2437
 
@@ -2257,7 +2442,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2257
2442
  *
2258
2443
  * It is captured by the store action that opens the dialog rather than by
2259
2444
  * the dialog itself: React focuses the `autoFocus`ed Cancel button while it
2260
- * commits, so by the time an effect runs the opener is already gone.
2445
+ * commits, so by the time the dialog is mounted the opener is already gone.
2261
2446
  */
2262
2447
  let confirmOpener = null
2263
2448
 
@@ -2272,26 +2457,29 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2272
2457
  confirmOpener = typeof document !== 'undefined' && document !== null ? document.activeElement : null
2273
2458
  }
2274
2459
 
2460
+ /**
2461
+ * Hand focus back one macrotask after the close. The host's Settings modal
2462
+ * moves focus on the same Escape, so a restore that runs inside the close is
2463
+ * overwritten and the user lands on the host's Close button instead.
2464
+ */
2465
+ function restoreConfirmOpener() {
2466
+ const opener = confirmOpener
2467
+ confirmOpener = null
2468
+ if (opener === null || typeof opener.focus !== 'function') return
2469
+ setTimeout(() => opener.focus(), 0)
2470
+ }
2471
+
2275
2472
  /**
2276
2473
  * One small modal confirmation, rendered unconditionally by its owner (and
2277
2474
  * `null` when closed) so React's hook count never changes between renders.
2278
- * Hook-free but for the focus restore, which is the one thing it cannot do
2279
- * with markup alone.
2475
+ * Hook-free: focus is restored by `closeConfirm`, the one path out.
2280
2476
  *
2281
2477
  * Cancel comes first and takes focus: the safe answer is the one a keyboard
2282
2478
  * or screen-reader user reaches without looking.
2283
2479
  */
2284
2480
  function ConfirmDialog(kit, { open, title, body, confirmLabel, onConfirm, onCancel }) {
2285
2481
  const { t } = kit
2286
- const isOpen = open === true
2287
- useEffect(() => {
2288
- if (!isOpen || typeof document === 'undefined') return undefined
2289
- const opener = confirmOpener
2290
- return () => {
2291
- if (opener !== null && typeof opener.focus === 'function') opener.focus()
2292
- }
2293
- }, [isOpen])
2294
- if (!isOpen) return null
2482
+ if (open !== true) return null
2295
2483
  const cancel = typeof onCancel === 'function' ? onCancel : () => {}
2296
2484
  return h('div', {
2297
2485
  className: 'tacit-modal-backdrop',
@@ -2318,7 +2506,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2318
2506
  },
2319
2507
  },
2320
2508
  h('div', {
2321
- className: 'tacit-modal-card',
2509
+ className: 'tacit-modal-card tacit-modal-card-sm',
2322
2510
  role: 'dialog',
2323
2511
  'aria-modal': 'true',
2324
2512
  'aria-labelledby': 'tacit-confirm-title',
@@ -2336,9 +2524,91 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2336
2524
 
2337
2525
  function DirectivesEditor(kit) {
2338
2526
  const { t } = kit
2527
+ const fmtDay = (ms) => {
2528
+ const date = new Date(Number(ms))
2529
+ return !(Number(ms) > 0) || Number.isNaN(date.getTime()) ? t('steer.receiptNever') : date.toISOString().slice(0, 10)
2530
+ }
2531
+ const pct = (value) => (typeof value === 'number' && value >= 0 ? Math.round(value * 100) + '%' : t('steer.receiptNever'))
2532
+ const triggerName = (name) => {
2533
+ const key = 'trigger.' + name
2534
+ const label = t(key)
2535
+ return label === key ? name : label
2536
+ }
2537
+ const copyReceipt = (receipt) => {
2538
+ if (typeof navigator === 'undefined' || navigator === null || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return
2539
+ navigator.clipboard.writeText(JSON.stringify(receipt, null, 2)).catch(() => {})
2540
+ }
2541
+ const metaRow = (label, value) => [h('dt', { key: label + '-t' }, label), h('dd', { key: label + '-d' }, value)]
2542
+ /** A null `note` marks an absent value ("never", "no ledger run"), shown quiet rather than as a headline figure. */
2543
+ const tile = (key, label, value, note) => h('div', { key, className: 'tacit-tile tacit-receipt-tile' },
2544
+ h('span', { className: 'tacit-tile-label' }, label),
2545
+ h('span', { className: 'tacit-tile-value' + (note === null ? ' tacit-tile-muted' : '') }, value),
2546
+ note === null ? null : h('span', { className: 'tacit-receipt-tile-note' }, note))
2547
+ const step = (key, label, ms) => h('li', { key, className: 'tacit-timeline-step' + (Number(ms) > 0 ? '' : ' tacit-timeline-never') },
2548
+ h('span', { className: 'tacit-timeline-label' }, label),
2549
+ h('span', { className: 'tacit-timeline-value' }, fmtDay(ms)))
2550
+ /**
2551
+ * What the receipt route returned for this directive, layered: three
2552
+ * tiles, a lifecycle timeline, the trigger chips, then the identity grid;
2553
+ * never any prompt text.
2554
+ */
2555
+ function Receipt(receipt) {
2556
+ if (receipt === null || typeof receipt === 'object' === false) return h('div', { className: 'tacit-panel-hint' }, t('steer.receiptLoading'))
2557
+ const triggers = Object.entries(receipt.triggers !== null && typeof receipt.triggers === 'object' ? receipt.triggers : {})
2558
+ const evidence = receipt.evidence !== null && typeof receipt.evidence === 'object' ? receipt.evidence : { turns: 0, conversations: 0 }
2559
+ const cost = receipt.cost !== null && typeof receipt.cost === 'object' ? receipt.cost : { usd: null, calls: 0 }
2560
+ const trial = receipt.trial !== null && typeof receipt.trial === 'object' ? receipt.trial : null
2561
+ return h('div', { className: 'tacit-receipt' },
2562
+ h('div', { className: 'tacit-tiles tacit-receipt-tiles' },
2563
+ tile('evidence', t('steer.receiptEvidence'), String(evidence.turns),
2564
+ t('steer.receiptEvidenceValue', { turns: String(evidence.turns), conversations: String(evidence.conversations) })),
2565
+ tile('trial', t('steer.receiptTrial'),
2566
+ trial === null ? t('steer.receiptNever') : t('steer.receiptTurns', { n: String(trial.turns) }),
2567
+ trial === null ? null : t('steer.receiptTrialNote', {
2568
+ started: fmtDay(trial.startedAt), messy: pct(trial.baselineMessyRate), corrected: pct(trial.baselineCorrectionRate) })),
2569
+ tile('cost', t('steer.receiptCost'),
2570
+ cost.usd === null ? t('steer.receiptNoRun') : fmtUsd(cost.usd),
2571
+ cost.usd === null ? null : t('steer.receiptCalls', { n: String(cost.calls) }))),
2572
+ h('ol', { className: 'tacit-timeline' },
2573
+ step('created', t('steer.receiptCreated'), receipt.createdAt),
2574
+ step('updated', t('steer.receiptUpdated'), receipt.updatedAt),
2575
+ step('evaluated', t('steer.receiptEvaluated'), receipt.evaluatedAt)),
2576
+ h('div', { className: 'tacit-receipt-row' },
2577
+ h('span', { className: 'tacit-receipt-label' }, t('steer.receiptTriggers')),
2578
+ triggers.length === 0
2579
+ ? h('span', { className: 'tacit-chip tacit-chip-muted' }, t('steer.receiptNever'))
2580
+ : triggers.map(([name, count]) => h('span', { key: name, className: 'tacit-chip' }, triggerName(name) + ' ' + String(count)))),
2581
+ h('dl', { className: 'tacit-receipt-meta' },
2582
+ ...metaRow(t('steer.receiptId'), String(receipt.id)),
2583
+ ...metaRow(t('steer.receiptVersion'), String(receipt.version)),
2584
+ ...metaRow(t('steer.receiptScope'), typeof receipt.scope === 'string' && receipt.scope.length > 0 ? receipt.scope : t('steer.everywhere')),
2585
+ ...metaRow(t('steer.receiptSource'), receipt.source === 'user' ? t('steer.user') : t('steer.distilled')),
2586
+ ...metaRow(t('steer.receiptStatus'), String(receipt.status))),
2587
+ h('div', { className: 'tacit-receipt-actions' },
2588
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => copyReceipt(receipt) }, t('steer.receiptCopy'))))
2589
+ }
2339
2590
  return function DirectivesEditorView(props) {
2340
- const { config, directives, steering } = props
2591
+ const { config, steering } = props
2592
+ const directives = (Array.isArray(props.directives) ? props.directives : []).filter((entry) => entry.status !== 'removed')
2593
+ const receipts = rootStore.receipts !== null && typeof rootStore.receipts === 'object' ? rootStore.receipts : {}
2594
+ const reviewOn = config !== null && config.reviewCandidates === true
2341
2595
  const workspaces = Array.isArray(props.workspaces) ? props.workspaces : []
2596
+ const seenAt = props.seenAt !== null && typeof props.seenAt === 'object' ? props.seenAt : {}
2597
+ const liveScope = (scope) => workspaces.some((entry) => entry.cwd === scope || String(entry.cwd).startsWith(scope + '/'))
2598
+ const notSeenSince = (entry) => {
2599
+ const stamp = typeof seenAt[entry.workspace] === 'number' ? seenAt[entry.workspace] : entry.createdAt
2600
+ const date = new Date(Number(stamp))
2601
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
2602
+ }
2603
+ const scopeOptions = (current) => {
2604
+ const options = [
2605
+ h('option', { key: '-', value: '-' }, t('steer.moveTo')),
2606
+ h('option', { key: '', value: '' }, t('steer.everywhere')),
2607
+ ]
2608
+ for (const entry of workspaces) options.push(h('option', { key: entry.cwd, value: entry.cwd }, entry.label))
2609
+ if (current.length > 0 && !workspaces.some((entry) => entry.cwd === current)) options.push(h('option', { key: current, value: current }, labelOf(current)))
2610
+ return options
2611
+ }
2342
2612
  const [draft, setDraft] = useState('')
2343
2613
  const [scope, setScope] = useState('')
2344
2614
  const labelOf = (cwd) => {
@@ -2361,25 +2631,49 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2361
2631
  setEnrichOn(next)
2362
2632
  updateRootConfig({ enrichPrompts: next })
2363
2633
  }
2634
+ const toggleReview = () => updateRootConfig({ reviewCandidates: !reviewOn })
2364
2635
  const onAdd = () => {
2365
2636
  const text = draft.trim()
2366
2637
  if (text.length === 0) return
2367
2638
  setDraft('')
2368
2639
  editDirectives({ action: 'add', text: text.slice(0, 300), ...(scope.length > 0 ? { workspace: scope } : {}) })
2369
2640
  }
2370
- return h('div', { className: 'tacit-panel-section', 'data-testid': 'tacit-steering' },
2371
- h('div', { className: 'tacit-report-title' }, t('steer.title')),
2372
- h('div', { className: 'tacit-panel-hint' }, t('steer.desc')),
2373
- h('div', { className: 'tacit-settings-row' },
2374
- h('label', { className: 'tacit-settings-label' }, t('steer.toggle')),
2375
- h('input', { type: 'checkbox', checked: steerOn, onChange: toggleSteer })),
2376
- h('div', { className: 'tacit-settings-row' },
2377
- h('label', { className: 'tacit-settings-label' }, t('steer.enrich')),
2378
- h('input', { type: 'checkbox', checked: enrichOn, onChange: toggleEnrich })),
2379
- directives.length === 0
2380
- ? h('div', { className: 'tacit-empty' }, t('steer.empty'))
2381
- : h('div', { className: 'tacit-rules-list' },
2382
- directives.map((entry) => h('div', { key: entry.id, className: 'tacit-rule tacit-directive' + (entry.enabled === false ? ' tacit-directive-off' : '') },
2641
+ /** The lifecycle badge: one colour per status, the label the chips used to carry. */
2642
+ const statusOf = (entry) => {
2643
+ if (entry.status === 'queued') return { kind: 'queued', label: t('steer.queued') }
2644
+ if (entry.status === 'candidate') {
2645
+ return {
2646
+ kind: 'trial',
2647
+ label: t('steer.trial', {
2648
+ n: String(entry.trial !== null && typeof entry.trial === 'object' && typeof entry.trial.turns === 'number' ? entry.trial.turns : 0),
2649
+ total: String(config !== null && typeof config.directiveTrialTurns === 'number' ? config.directiveTrialTurns : 10),
2650
+ }),
2651
+ }
2652
+ }
2653
+ if (entry.status === 'retired') return { kind: 'retired', label: t('steer.retired', { reason: String(entry.retiredReason || '') }) }
2654
+ return { kind: 'active', label: t('steer.active') }
2655
+ }
2656
+ const directiveCard = (entry) => {
2657
+ const status = statusOf(entry)
2658
+ const scoped = typeof entry.workspace === 'string' && entry.workspace.length > 0
2659
+ return h('div', { key: entry.id, className: 'tacit-dir tacit-dir-' + status.kind + (entry.enabled === false ? ' tacit-dir-off' : '') },
2660
+ h('div', { className: 'tacit-dir-head' },
2661
+ h('span', { className: 'tacit-badge tacit-badge-' + status.kind },
2662
+ h('span', { className: 'tacit-badge-dot', 'aria-hidden': 'true' }),
2663
+ status.label),
2664
+ h('span', { className: 'tacit-chip' }, entry.source === 'user' ? t('steer.user') : t('steer.distilled')),
2665
+ scoped
2666
+ ? h('span', { className: 'tacit-chip tacit-chip-scope', title: entry.workspace }, t('steer.workspace', { name: labelOf(entry.workspace) }))
2667
+ : null,
2668
+ scoped && !liveScope(entry.workspace)
2669
+ ? h('span', { className: 'tacit-chip tacit-chip-muted', title: entry.workspace }, t('steer.notSeen', { date: notSeenSince(entry) }))
2670
+ : null,
2671
+ reviewOn && entry.status === 'queued' && !(entry.approvedAt > 0)
2672
+ ? h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm tacit-btn-primary tacit-dir-start', onClick: () => editDirectives({ action: 'start-trial', id: entry.id }) }, t('steer.startTrial'))
2673
+ : null),
2674
+ h('div', { className: 'tacit-dir-text' }, String(entry.text)),
2675
+ h('div', { className: 'tacit-dir-actions' },
2676
+ h('label', { className: 'tacit-dir-enabled' },
2383
2677
  h('input', {
2384
2678
  type: 'checkbox',
2385
2679
  className: 'tacit-check',
@@ -2387,23 +2681,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2387
2681
  checked: entry.enabled !== false,
2388
2682
  onChange: () => editDirectives({ action: 'toggle', id: entry.id, enabled: entry.enabled === false }),
2389
2683
  }),
2390
- h('span', { className: 'tacit-directive-text' }, String(entry.text)),
2391
- h('span', { className: 'tacit-chip' }, entry.source === 'user' ? t('steer.user') : t('steer.distilled')),
2392
- typeof entry.workspace === 'string' && entry.workspace.length > 0
2393
- ? h('span', { className: 'tacit-chip tacit-chip-scope', title: entry.workspace }, t('steer.workspace', { name: labelOf(entry.workspace) }))
2394
- : null,
2395
- entry.status === 'queued'
2396
- ? h('span', { className: 'tacit-chip tacit-chip-muted' }, t('steer.queued'))
2397
- : entry.status === 'candidate'
2398
- ? h('span', { className: 'tacit-chip tacit-chip-trial' }, t('steer.trial', {
2399
- n: String(entry.trial !== null && typeof entry.trial === 'object' && typeof entry.trial.turns === 'number' ? entry.trial.turns : 0),
2400
- total: String(config !== null && typeof config.directiveTrialTurns === 'number' ? config.directiveTrialTurns : 10),
2401
- }))
2402
- : entry.status === 'retired'
2403
- ? h('span', { className: 'tacit-chip tacit-chip-warn' }, t('steer.retired', { reason: String(entry.retiredReason || '') }))
2404
- : h('span', { className: 'tacit-chip tacit-chip-ok' }, t('steer.active')),
2405
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'remove', id: entry.id }) }, t('steer.remove'))))),
2406
- h('div', { className: 'tacit-settings-row' },
2684
+ t('steer.enabled')),
2685
+ h('select', {
2686
+ className: 'tacit-input tacit-select tacit-select-sm',
2687
+ 'aria-label': t('steer.moveTo'),
2688
+ onChange: (event) => {
2689
+ if (event.target.value !== '-') editDirectives({ action: 'rescope', id: entry.id, workspace: event.target.value })
2690
+ },
2691
+ }, ...scopeOptions(typeof entry.workspace === 'string' ? entry.workspace : '')),
2692
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm tacit-btn-quiet tacit-dir-remove', onClick: () => editDirectives({ action: 'remove', id: entry.id }) }, t('steer.remove'))),
2693
+ h('details', {
2694
+ className: 'tacit-dir-receipt',
2695
+ 'data-testid': 'tacit-receipt',
2696
+ 'data-directive-id': entry.id,
2697
+ onToggle: (event) => {
2698
+ if (event.target.open) fetchReceipt(entry.id)
2699
+ },
2700
+ },
2701
+ h('summary', null, t('steer.receipt')),
2702
+ Receipt(receipts[entry.id] ?? null)))
2703
+ }
2704
+ return h('div', { className: 'tacit-panel-section', 'data-testid': 'tacit-steering' },
2705
+ h('div', { className: 'tacit-report-title' }, t('steer.title')),
2706
+ h('div', { className: 'tacit-panel-hint' }, t('steer.desc')),
2707
+ directives.length === 0
2708
+ ? h('div', { className: 'tacit-empty' }, t('steer.empty'))
2709
+ : h('div', { className: 'tacit-dir-list' }, directives.map(directiveCard)),
2710
+ h('div', { className: 'tacit-dir-add' },
2407
2711
  h('input', {
2408
2712
  className: 'tacit-input tacit-directive-input',
2409
2713
  type: 'text',
@@ -2426,7 +2730,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2426
2730
  h('option', { value: '' }, t('steer.everywhere')),
2427
2731
  ...workspaces.map((entry) => h('option', { key: entry.cwd, value: entry.cwd }, entry.label)))
2428
2732
  : null,
2429
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', disabled: draft.trim().length === 0, onClick: onAdd }, t('steer.add'))),
2733
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm tacit-btn-primary', disabled: draft.trim().length === 0, onClick: onAdd }, t('steer.add'))),
2734
+ h('div', { className: 'tacit-options' },
2735
+ h('div', { className: 'tacit-report-title' }, t('steer.options')),
2736
+ OptionRow(t('steer.toggle'), steerOn, toggleSteer),
2737
+ OptionRow(t('steer.enrich'), enrichOn, toggleEnrich),
2738
+ OptionRow(t('steer.review'), reviewOn, toggleReview)),
2430
2739
  steering !== null && typeof steering === 'object' && typeof steering.text === 'string' && steering.text.length > 0
2431
2740
  ? h('details', { className: 'tacit-preview' },
2432
2741
  h('summary', null, t('steer.preview')),
@@ -2576,19 +2885,15 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2576
2885
  h('div', { className: 'tacit-settings-notice', role: 'status' }, notice === null ? '' : notice.text),
2577
2886
  card('overview', [
2578
2887
  StatusCard(kit, { config, profile, auto: rootStore.auto, trend: rootStore.trend }),
2579
- config !== null && typeof config.model === 'string' && config.model.length > 0
2580
- ? h('div', { className: 'tacit-settings-row' },
2581
- h('span', { className: 'tacit-chip' }, t('overview.model', { model: config.model })))
2582
- : null,
2583
- h('div', { className: 'tacit-settings-row' },
2888
+ h('div', { className: 'tacit-inline' },
2584
2889
  BootstrapButton(kit, {
2585
2890
  bootstrap: rootStore.bootstrap,
2586
2891
  // Only a loaded preview may disable the button: before one
2587
2892
  // lands, "0 eligible" is an absence of data, not a fact.
2588
2893
  disabled: preview !== null && preview.eligible === 0,
2589
2894
  onClick: () => bootstrapAll(t),
2590
- }),
2591
- h('span', { className: 'tacit-panel-hint' }, t('bootstrap.hint'))),
2895
+ })),
2896
+ h('p', { className: 'tacit-panel-hint' }, t('bootstrap.hint')),
2592
2897
  // What the run would actually cost, priced from the ledger once it
2593
2898
  // holds enough analyses; until the preview lands, the documented
2594
2899
  // figure stands in.
@@ -2627,40 +2932,40 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2627
2932
  }),
2628
2933
  ], { summary: pricingSummary(kit, usagePricing) }),
2629
2934
  card('learning', [
2630
- h('div', { className: 'tacit-settings-row' },
2631
- h('label', { className: 'tacit-settings-label' }, t('settings.model')),
2632
- h('select', {
2633
- className: 'tacit-select',
2634
- value: model,
2635
- onChange: (event) => applyModel(event.target.value),
2636
- },
2637
- h('option', { value: 'deepseek-v4-flash' }, 'deepseek-v4-flash'),
2638
- h('option', { value: 'deepseek-v4-pro' }, 'deepseek-v4-pro'))),
2639
- h('div', { className: 'tacit-settings-row' },
2640
- h('label', { className: 'tacit-settings-label' }, t('settings.auto')),
2641
- h('input', { type: 'checkbox', checked: auto, onChange: toggleAuto })),
2642
- h('div', { className: 'tacit-settings-row' },
2643
- h('label', { className: 'tacit-settings-label' }, t('settings.learnGood')),
2644
- h('input', { type: 'checkbox', checked: learnGood, onChange: toggleLearnGood })),
2645
- h('div', { className: 'tacit-settings-row' },
2646
- h('label', { className: 'tacit-settings-label' }, t('settings.budget')),
2935
+ FieldRow(t('settings.model'), h('select', {
2936
+ id: 'tacit-learning-model',
2937
+ className: 'tacit-select',
2938
+ value: model,
2939
+ onChange: (event) => applyModel(event.target.value),
2940
+ },
2941
+ h('option', { value: 'deepseek-v4-flash' }, 'deepseek-v4-flash'),
2942
+ h('option', { value: 'deepseek-v4-pro' }, 'deepseek-v4-pro')), 'tacit-learning-model'),
2943
+ FieldRow(t('settings.budget'), [
2647
2944
  h('input', {
2945
+ key: 'input',
2946
+ id: 'tacit-learning-budget',
2648
2947
  className: 'tacit-input',
2649
2948
  type: 'number',
2650
2949
  min: 0,
2651
2950
  value: budgetText,
2652
2951
  onChange: (event) => setBudgetText(event.target.value),
2653
2952
  }),
2654
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: applyBudget }, t('settings.apply'))),
2655
- ]),
2953
+ h('button', { key: 'apply', type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: applyBudget }, t('settings.apply')),
2954
+ ], 'tacit-learning-budget'),
2955
+ h('div', { className: 'tacit-options' },
2956
+ OptionRow(t('settings.auto'), auto, toggleAuto),
2957
+ OptionRow(t('settings.learnGood'), learnGood, toggleLearnGood)),
2958
+ // The model in force reads from the collapsed head, where it is set.
2959
+ ], config !== null && typeof config.model === 'string' && config.model.length > 0
2960
+ ? { summary: t('overview.model', { model: config.model }) }
2961
+ : undefined),
2656
2962
  card('guidance', [
2657
2963
  h(DirectivesEditorView, {
2658
- workspaces: rootStore.workspaces, config, directives, steering: rootStore.steering }),
2659
- ]),
2964
+ workspaces: rootStore.workspaces, config, directives, steering: rootStore.steering,
2965
+ seenAt: rootStore.profile !== null && typeof rootStore.profile === 'object' ? rootStore.profile.workspaceSeenAt : null }),
2966
+ ], { count: directives.filter((entry) => entry.status !== 'removed').length }),
2660
2967
  card('improve', [
2661
- h('div', { className: 'tacit-settings-row' },
2662
- h('label', { className: 'tacit-settings-label' }, t('settings.live')),
2663
- h('input', { type: 'checkbox', checked: live, onChange: toggleLive })),
2968
+ OptionRow(t('settings.live'), live, toggleLive),
2664
2969
  h('div', { className: 'tacit-panel-section' },
2665
2970
  h('div', { className: 'tacit-report-title' }, t('panel.styleRules')),
2666
2971
  styleRules.length === 0
@@ -2690,18 +2995,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2690
2995
  ], { count: coached.length }),
2691
2996
  card('privacy', [
2692
2997
  h('p', { className: 'tacit-panel-hint' }, t('privacy.stored')),
2693
- h('div', { className: 'tacit-settings-row' },
2694
- h('label', { className: 'tacit-settings-label', htmlFor: 'tacit-retention' }, t('privacy.retention')),
2695
- h('select', {
2696
- id: 'tacit-retention',
2697
- className: 'tacit-select',
2698
- value: String(retention),
2699
- onChange: (event) => updateRootConfig({ costHistoryDays: Number(event.target.value) }),
2700
- },
2701
- ...retentionDays.map((days) => h('option', { key: days, value: String(days) }, String(days))))),
2702
- h('div', { className: 'tacit-settings-row' },
2703
- h('label', { className: 'tacit-settings-label', htmlFor: 'tacit-warn-daily' }, t('privacy.warnDaily')),
2998
+ FieldRow(t('privacy.retention'), h('select', {
2999
+ id: 'tacit-retention',
3000
+ className: 'tacit-select',
3001
+ value: String(retention),
3002
+ onChange: (event) => updateRootConfig({ costHistoryDays: Number(event.target.value) }),
3003
+ },
3004
+ ...retentionDays.map((days) => h('option', { key: days, value: String(days) }, String(days)))), 'tacit-retention'),
3005
+ FieldRow(t('privacy.warnDaily'), [
2704
3006
  h('input', {
3007
+ key: 'input',
2705
3008
  id: 'tacit-warn-daily',
2706
3009
  className: 'tacit-input',
2707
3010
  type: 'number',
@@ -2711,16 +3014,18 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2711
3014
  onChange: (event) => setDailyText(event.target.value),
2712
3015
  }),
2713
3016
  h('button', {
3017
+ key: 'apply',
2714
3018
  type: 'button',
2715
3019
  className: 'tacit-btn tacit-btn-sm',
2716
3020
  // Two identically labelled buttons in one card: the accessible
2717
3021
  // name has to say which threshold each one applies.
2718
3022
  'aria-label': t('privacy.applyTo', { action: t('privacy.apply'), field: t('privacy.warnDaily') }),
2719
3023
  onClick: () => applyWarn('costWarnDailyUsd', dailyText, setDailyText),
2720
- }, t('privacy.apply'))),
2721
- h('div', { className: 'tacit-settings-row' },
2722
- h('label', { className: 'tacit-settings-label', htmlFor: 'tacit-warn-monthly' }, t('privacy.warnMonthly')),
3024
+ }, t('privacy.apply')),
3025
+ ], 'tacit-warn-daily'),
3026
+ FieldRow(t('privacy.warnMonthly'), [
2723
3027
  h('input', {
3028
+ key: 'input',
2724
3029
  id: 'tacit-warn-monthly',
2725
3030
  className: 'tacit-input',
2726
3031
  type: 'number',
@@ -2730,18 +3035,20 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2730
3035
  onChange: (event) => setMonthlyText(event.target.value),
2731
3036
  }),
2732
3037
  h('button', {
3038
+ key: 'apply',
2733
3039
  type: 'button',
2734
3040
  className: 'tacit-btn tacit-btn-sm',
2735
3041
  // Two identically labelled buttons in one card: the accessible
2736
3042
  // name has to say which threshold each one applies.
2737
3043
  'aria-label': t('privacy.applyTo', { action: t('privacy.apply'), field: t('privacy.warnMonthly') }),
2738
3044
  onClick: () => applyWarn('costWarnMonthlyUsd', monthlyText, setMonthlyText),
2739
- }, t('privacy.apply'))),
3045
+ }, t('privacy.apply')),
3046
+ ], 'tacit-warn-monthly'),
2740
3047
  h('p', { className: 'tacit-panel-hint' }, t('privacy.warnHint')),
2741
3048
  // Both clears are irreversible, so both go through the dialog.
2742
- h('div', { className: 'tacit-settings-row' },
2743
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => openConfirm('reports') }, t('settings.clear')),
2744
- h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => openConfirm('usage') }, t('privacy.clearUsage'))),
3049
+ h('div', { className: 'tacit-inline' },
3050
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm tacit-btn-quiet', onClick: () => openConfirm('reports') }, t('settings.clear')),
3051
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm tacit-btn-quiet', onClick: () => openConfirm('usage') }, t('privacy.clearUsage'))),
2745
3052
  ]),
2746
3053
  // Always rendered (null while closed) so its one effect keeps this
2747
3054
  // component's hook count stable.
@@ -2792,10 +3099,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2792
3099
  typeof note === 'string' && note.length > 0 ? h('span', { className: 'tacit-tile-note' }, note) : null)
2793
3100
  }
2794
3101
 
2795
- /** Money tile: the period's spend plus its unpriced-call footnote. */
2796
- function SpendTile(kit, id, label, totals) {
2797
- const note = totals.unpricedCalls > 0 ? kit.t('usage.unpricedShort', { n: String(totals.unpricedCalls) }) : ''
2798
- return UsageTile(id, label, usageSpend(kit, totals), note)
3102
+ /** Money tile: the period's spend, its unpriced-call footnote, and any second note that period carries. */
3103
+ function SpendTile(kit, id, label, totals, extraNote) {
3104
+ const notes = []
3105
+ if (totals.unpricedCalls > 0) notes.push(kit.t('usage.unpricedShort', { n: String(totals.unpricedCalls) }))
3106
+ if (typeof extraNote === 'string' && extraNote.length > 0) notes.push(extraNote)
3107
+ return UsageTile(id, label, usageSpend(kit, totals), notes.join(' · '))
2799
3108
  }
2800
3109
 
2801
3110
  /**
@@ -2858,18 +3167,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2858
3167
  seriesButton('30', t('usage.series30'))))
2859
3168
  }
2860
3169
 
2861
- /** Spend per operation, costliest first, each row ending in a share bar. */
2862
- function UsageBreakdown(kit, { byType }) {
3170
+ /**
3171
+ * `trigger` is a free string on the wire, so a host that starts tagging
3172
+ * runs with something this dictionary has never heard of shows the tag
3173
+ * itself rather than a raw `trigger.<key>` lookup miss.
3174
+ */
3175
+ function triggerLabel(t, trigger) {
3176
+ const key = 'trigger.' + trigger
3177
+ const label = t(key)
3178
+ return label === key ? trigger : label
3179
+ }
3180
+
3181
+ /**
3182
+ * Spend across one bucket record, costliest first, each row ending in a
3183
+ * share bar. `label` turns a bucket key into its display name, which is a
3184
+ * dictionary lookup for the closed sets and the raw key for the open ones.
3185
+ */
3186
+ function UsageBreakdown(kit, { buckets, title, label }) {
2863
3187
  const { t } = kit
2864
- const rows = Object.entries(byType).sort((a, b) => b[1].usdKnown - a[1].usdKnown)
3188
+ const rows = Object.entries(buckets).sort((a, b) => b[1].usdKnown - a[1].usdKnown)
2865
3189
  if (rows.length === 0) return null
2866
3190
  const top = rows[0][1].usdKnown
2867
- return h('div', { className: 'tacit-usage-breakdown' },
2868
- h('div', { className: 'tacit-report-title' }, t('usage.breakdown')),
3191
+ return h('div', { key: title, className: 'tacit-usage-breakdown' },
3192
+ h('div', { className: 'tacit-report-title' }, t(title)),
2869
3193
  rows.map(([type, totals]) => {
2870
3194
  const share = top > 0 ? Math.round((totals.usdKnown / top) * 100) : 0
2871
3195
  return h('div', { key: type, className: 'tacit-breakdown-row' },
2872
- h('span', { className: 'tacit-breakdown-name' }, t('runtype.' + type)),
3196
+ h('span', { className: 'tacit-breakdown-name' }, label(type)),
2873
3197
  h('span', { className: 'tacit-breakdown-calls' }, fmtTokens(totals.billedCalls)),
2874
3198
  h('span', { className: 'tacit-breakdown-tokens' }, fmtTokens(tokensTotal(totals.tokens))),
2875
3199
  h('span', { className: 'tacit-breakdown-usd' }, usageSpend(kit, totals)),
@@ -3009,7 +3333,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3009
3333
  cell('model', item.model.length > 0 ? item.model : '—'),
3010
3334
  cell('status', h('span', { className: 'tacit-chip tacit-status-' + item.status }, t('status.' + item.status))),
3011
3335
  cell('calls', fmtTokens(item.billedCalls)),
3012
- cell('tokens', fmtTokens(tokensTotal(item.tokens))),
3336
+ cell('tokens', h('span', { className: 'tacit-usage-tokens' },
3337
+ h('span', null, fmtTokens(tokensTotal(item.tokens))),
3338
+ h('span', { className: 'tacit-usage-split' }, fmtTokensSplit(item.tokens)))),
3013
3339
  cell('cost', usageSpend(kit, item)))
3014
3340
  }
3015
3341
 
@@ -3070,19 +3396,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3070
3396
  return h('div', { className: 'tacit-usage' },
3071
3397
  h('p', { className: 'tacit-panel-hint' }, t('usage.label')),
3072
3398
  h('div', { className: 'tacit-tiles' },
3073
- SpendTile(kit, 'today', t('usage.today'), usage.today),
3399
+ SpendTile(kit, 'today', t('usage.today'), usage.today,
3400
+ t('usage.autoToday', { n: String(usage.auto.today), budget: String(usage.auto.budget) })),
3074
3401
  SpendTile(kit, 'month', t('usage.month'), usage.month),
3075
3402
  SpendTile(kit, 'last30', t('usage.last30'), last30),
3076
3403
  SpendTile(kit, 'lifetime', t('usage.since', { day: since }), usage.lifetime)),
3077
3404
  h('div', { className: 'tacit-tiles' },
3078
3405
  UsageTile('calls', t('usage.calls'), fmtTokens(last30.billedCalls)),
3406
+ UsageTile('tokens', t('usage.tokens'), fmtTokensSplit(last30.tokens)),
3079
3407
  UsageTile('avg', t('usage.avgAnalysis'), last30.avgAnalysisUsd === null
3080
3408
  ? t('usage.priceUnavailable')
3081
3409
  : fmtUsd(last30.avgAnalysisUsd)),
3082
3410
  UsageTile('cached', t('usage.cachedRate'), fmtPct(last30.cachedInputRate)),
3083
- UsageTile('unpriced', t('usage.unpriced'), fmtTokens(last30.unpricedCalls))),
3411
+ UsageTile('unpriced', t('usage.unpriced'), fmtTokens(last30.unpricedCalls)),
3412
+ UsageTile('failedRepair', t('usage.failedRepair'), fmtUsd(last30.failedOrRepairUsd),
3413
+ t('usage.failedRepairSplit', { failed: fmtUsd(last30.failedUsd), repair: fmtUsd(last30.repairUsd) }))),
3084
3414
  BarStrip(kit, { series: which === '7' ? usage.series7 : usage.series30, which, onSeries: props.onSeries }),
3085
- UsageBreakdown(kit, { byType: usage.byType }),
3415
+ UsageBreakdown(kit, { buckets: usage.byType, title: 'usage.breakdown', label: (type) => t('runtype.' + type) }),
3416
+ UsageBreakdown(kit, { buckets: usage.byProvider, title: 'usage.byRoute', label: (provider) => provider }),
3417
+ UsageBreakdown(kit, { buckets: usage.byTrigger, title: 'usage.byTrigger', label: (trigger) => triggerLabel(t, trigger) }),
3086
3418
  WarnBar(kit, { id: 'daily', warning: usage.warnings.daily }),
3087
3419
  WarnBar(kit, { id: 'monthly', warning: usage.warnings.monthly }),
3088
3420
  UsageFilters(kit, {
@@ -3237,17 +3569,19 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3237
3569
 
3238
3570
  const css = '.tacit-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}'
3239
3571
  + '.tacit-head{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}.tacit-head-title{font-size:14px;font-weight:600;margin-right:auto}'
3240
- + '.tacit-progress{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 12px;margin-bottom:12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}.tacit-progress-title{font-weight:600;color:var(--dsw-alias-label-primary)}'
3241
- + '.tacit-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:24px 8px;line-height:1.6}'
3572
+ + '.tacit-progress{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 12px;margin-bottom:12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}.tacit-progress-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-right:auto}'
3573
+ + '.tacit-empty{color:var(--dsw-alias-label-secondary);text-align:center;font-size:12px;line-height:1.6;max-width:46ch;margin-inline:auto;padding:18px 12px;border:1px dashed var(--dsw-alias-border-l1);border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 50%, transparent)}'
3242
3574
  + '.tacit-error{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 12px;margin-bottom:12px;overflow-wrap:anywhere;font-size:12px}'
3243
3575
  + '.tacit-btn{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;cursor:pointer;padding:4px 10px}.tacit-btn:hover{border-color:var(--dsw-alias-label-primary)}.tacit-btn:disabled{opacity:.55;cursor:default}'
3244
3576
  + '.tacit-btn-primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:transparent}.tacit-btn-primary:hover{border-color:transparent;filter:brightness(1.1)}'
3245
3577
  + '.tacit-btn-sm{padding:2px 8px;font-size:11px}'
3246
3578
  + '.tacit-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent)}.tacit-btn-danger:hover{border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}'
3247
3579
  + '.tacit-row{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 12px;margin-bottom:10px}'
3248
- + '.tacit-row-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 12px}.tacit-row-heading{display:flex;align-items:baseline;gap:8px;font-weight:600}.tacit-row-turn{font-size:13px}.tacit-row-time{color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:400}.tacit-row-live{color:var(--dsw-alias-state-success-primary);font-size:11px;font-weight:600}'
3249
- + '.tacit-row-chips{display:flex;flex-wrap:wrap;gap:4px;margin-left:auto}.tacit-chip{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:1px 7px;font-size:11px}.tacit-chip-warn{color:var(--dsw-alias-state-warn-primary)}'
3250
- + '.tacit-row-prompt{display:flex;align-items:flex-start;gap:8px;margin-top:8px}.tacit-row-prompt-text{font:inherit;font-size:12px;color:var(--dsw-alias-label-primary);background:none;border:0;cursor:pointer;text-align:left;padding:0;white-space:pre-wrap;word-break:break-word;flex:1;line-height:1.55}'
3580
+ + '.tacit-row-head{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px}.tacit-row-heading{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;font-weight:600;min-width:0}.tacit-row-turn{font-size:13px;font-variant-numeric:tabular-nums}.tacit-row-time{color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:400}.tacit-row-actions{display:flex;align-items:center;gap:6px;margin-left:auto}'
3581
+ + '.tacit-chip{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:1px 7px;font-size:11px}.tacit-chip-warn{color:var(--dsw-alias-state-warn-primary)}'
3582
+ // Turn facts read as one dot-separated line, not four bordered chips.
3583
+ + '.tacit-row-meta{display:flex;flex-wrap:wrap;align-items:center;margin-top:8px;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}.tacit-row-meta>span+span::before{content:"·";margin:0 7px;opacity:.6}.tacit-row-meta-warn{color:var(--dsw-alias-state-warn-primary)}'
3584
+ + '.tacit-row-prompt{display:flex;align-items:flex-start;gap:8px;margin-top:8px}.tacit-row-prompt-text{font:inherit;font-size:12px;color:var(--dsw-alias-label-primary);background:none;border:0;cursor:pointer;text-align:left;padding:0;white-space:pre-wrap;word-break:break-word;flex:1;line-height:1.55}.tacit-row-prompt-static{cursor:auto}.tacit-row-more{margin-left:8px;font-size:11px;color:var(--dsw-alias-brand-primary);white-space:nowrap}'
3251
3585
  + '.tacit-report{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px;display:flex;flex-direction:column;gap:6px}'
3252
3586
  + '.tacit-report-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary)}'
3253
3587
  + '.tacit-report-note{color:var(--dsw-alias-label-secondary);font-size:12px}'
@@ -3263,7 +3597,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3263
3597
  + '.tacit-problem-what{font-size:12px;color:var(--dsw-alias-label-primary);line-height:1.5}.tacit-problem-why{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.5}'
3264
3598
  + '.tacit-pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:inherit;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary)}'
3265
3599
  + '.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}'
3266
- + '.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}'
3600
+ + '.tacit-settings-title{font-size:12px;font-weight:600}'
3601
+ // One settings row everywhere: the label takes the slack and wraps, the
3602
+ // control stays right-aligned, and nothing needs a 150px minimum.
3603
+ + '.tacit-field{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}.tacit-field-label{flex:1 1 150px;min-width:0;font-size:12px;line-height:1.45;color:var(--dsw-alias-label-secondary)}.tacit-field-control{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-left:auto}'
3604
+ + '.tacit-inline,.tacit-settings-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}'
3267
3605
  + '.tacit-settings-notice{font-size:11px;color:var(--dsw-alias-state-success-primary)}'
3268
3606
  // The idle live region has to stay mounted to announce, but as an empty
3269
3607
  // flex item it still claims one of the panel's 10px gaps; -5px a side
@@ -3272,22 +3610,22 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3272
3610
  + '.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}'
3273
3611
  + '.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)}'
3274
3612
  + '.tacit-check{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);flex:none;margin:2px 4px 0 0}'
3275
- + '.tacit-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap}.tacit-toolbar-hint{flex:1;min-width:200px}.tacit-btn-quiet{color:var(--dsw-alias-label-secondary);background:transparent}.tacit-chip-muted{opacity:.7}.tacit-row-analyzed{border-left:3px solid var(--dsw-alias-brand-primary)}'
3276
- + '.tacit-progress-head{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.tacit-progress-count{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}'
3613
+ + '.tacit-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap}.tacit-toolbar-note{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary);margin-bottom:10px}.tacit-btn-quiet{color:var(--dsw-alias-label-secondary);background:transparent;border-color:transparent}.tacit-btn-quiet:hover{color:var(--dsw-alias-label-primary);border-color:transparent;background:var(--dsw-alias-interactive-bg-hover)}.tacit-chip-muted{opacity:.7}.tacit-row-analyzed{border-left:3px solid var(--dsw-alias-brand-primary)}'
3614
+ + '.tacit-progress-head{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;margin-bottom:6px}'
3277
3615
  + '.tacit-progress-text{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}'
3278
- + '.tacit-panel{display:flex;flex-direction:column;gap:10px}.tacit-panel-section{display:flex;flex-direction:column;gap:6px}.tacit-panel-hint{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.5}'
3616
+ + '.tacit-panel{display:flex;flex-direction:column;gap:10px}.tacit-panel-section{display:flex;flex-direction:column;gap:6px}.tacit-panel-hint{margin:0;font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.5}'
3279
3617
  + '.tacit-coached-list{display:flex;flex-direction:column;gap:6px}.tacit-coached-row{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 10px;display:flex;flex-direction:column;gap:3px}'
3280
3618
  + '.tacit-coached-meta{display:flex;align-items:baseline;gap:8px}.tacit-coached-turn{font-weight:600;font-size:12px}.tacit-coached-time{color:var(--dsw-alias-label-secondary);font-size:11px}'
3281
3619
  + '.tacit-coached-excerpt{font-size:12px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;line-height:1.5}.tacit-coached-improved{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.5;white-space:pre-wrap;word-break:break-word}'
3282
3620
  + ''
3283
3621
  + '.tacit-modal-backdrop{z-index:200;background:#00000073;display:flex;justify-content:center;align-items:center;position:fixed;inset:0}'
3284
- + '.tacit-modal-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:min(760px,100vw - 48px);max-height:min(84vh,800px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;padding:16px 18px 18px;font-size:13px;display:flex;flex-direction:column;gap:10px;overflow-y:auto}'
3622
+ + '.tacit-modal-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:min(760px,100vw - 48px);max-height:min(84vh,800px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;padding:16px 18px 18px;font-size:13px;display:flex;flex-direction:column;gap:10px;overflow-y:auto}.tacit-modal-card-sm{width:min(440px,100vw - 48px)}'
3285
3623
  + '.tacit-modal-head{display:flex;align-items:baseline;gap:8px}.tacit-modal-title{font-size:14px;font-weight:600;margin:0 auto 0 0}.tacit-modal-close{color:var(--dsw-alias-label-secondary);cursor:pointer;background:none;border:0;border-radius:6px;padding:2px 8px;font-family:inherit;font-size:18px;line-height:1}.tacit-modal-close:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}'
3286
3624
  + '.tacit-modal-pending{color:var(--dsw-alias-label-secondary);padding:16px 0;text-align:center}'
3287
3625
  + '.tacit-modal-unchanged{color:var(--dsw-alias-label-primary);padding:16px 0;text-align:center}'
3288
- + '.tacit-modal-cols{display:grid;grid-template-columns:1fr 1fr;gap:10px}.tacit-modal-col{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);padding:8px 10px;min-width:0;max-height:300px;overflow-y:auto}'
3626
+ + '.tacit-modal-body{display:flex;flex-direction:column;gap:10px}.tacit-modal-cols{display:grid;grid-template-columns:1fr 1fr;gap:10px}.tacit-modal-col{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);padding:8px 10px;min-width:0;max-height:300px;overflow-y:auto}'
3289
3627
  + '.tacit-modal-col-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:6px}'
3290
- + '.tacit-modal-rationale-text{font-size:12px;color:var(--dsw-alias-label-primary);line-height:1.6}.tacit-modal-savings{color:var(--dsw-alias-state-success-primary);font-size:12px;font-weight:600}'
3628
+ + '.tacit-modal-col-improved{border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 40%, transparent)}.tacit-modal-rationale{display:flex;flex-direction:column;gap:4px}.tacit-modal-rationale-text{font-size:12px;color:var(--dsw-alias-label-primary);line-height:1.6}.tacit-modal-savings{color:var(--dsw-alias-state-success-primary);font-size:12px;font-weight:600}'
3291
3629
  + '.tacit-modal-actions{display:flex;gap:8px;justify-content:flex-end}'
3292
3630
  + '.tacit-confirm-body{margin:0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary)}'
3293
3631
  + '.tacit-confirm-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:4px}'
@@ -3298,15 +3636,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3298
3636
  + '.tacit-feedback-noted{color:var(--dsw-alias-state-success-primary);font-weight:600}'
3299
3637
  + '.tacit-trend{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:6px}.tacit-chip-trigger,.tacit-coached-trigger{text-transform:uppercase;letter-spacing:.04em;font-size:10px}.tacit-coached-trigger{margin-left:auto}.tacit-row-enrichment{margin-top:8px;border-left:3px solid var(--dsw-alias-brand-primary);padding:4px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:6px}'
3300
3638
  + '.tacit-chip-trial{color:var(--dsw-alias-brand-primary)}.tacit-chip-ok{color:var(--dsw-alias-state-success-primary)}.tacit-chip-scope{color:var(--dsw-alias-brand-primary)}.tacit-select{max-width:11rem}'
3301
- + '.tacit-directive{display:flex;align-items:center;gap:8px}.tacit-directive-text{flex:1;min-width:0}.tacit-directive-off .tacit-directive-text{opacity:.5;text-decoration:line-through}.tacit-directive-input{width:min(420px,100%);flex:1}.tacit-preview summary{cursor:pointer;font-size:11px;color:var(--dsw-alias-label-secondary)}.tacit-preview pre{margin-top:6px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:8px 10px}'
3639
+ + '.tacit-directive-input{width:min(420px,100%);flex:1}.tacit-preview summary{cursor:pointer;font-size:11px;color:var(--dsw-alias-label-secondary)}.tacit-preview pre{margin-top:6px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:8px 10px}'
3302
3640
  + '.tacit-rules-list{display:flex;flex-direction:column;gap:6px}.tacit-rule{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-brand-primary);border-radius:6px;padding:6px 10px;font-size:12px;line-height:1.55;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}'
3303
3641
  + '.tacit-card{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}'
3304
- + '.tacit-card-head{width:100%;display:flex;align-items:center;gap:8px;text-align:left;padding:10px 12px;background:none;border:0;cursor:pointer;font:inherit;color:inherit}'
3642
+ + '.tacit-card-head{width:100%;display:flex;align-items:center;gap:8px;text-align:left;padding:10px 12px;background:none;border:0;border-radius:7px;cursor:pointer;font:inherit;color:inherit}.tacit-card-head:hover{background:var(--dsw-alias-interactive-bg-hover)}'
3305
3643
  + '.tacit-card-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary)}'
3306
3644
  + '.tacit-card-title{font-size:13px;font-weight:600}.tacit-card-summary{font-size:11px;color:var(--dsw-alias-label-secondary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'
3307
3645
  + '.tacit-card-count{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:1px 7px;font-size:11px;font-variant-numeric:tabular-nums}'
3308
- + '.tacit-card-chevron{margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:11px}'
3309
- + '.tacit-card-body{padding:0 12px 12px}'
3646
+ // One disclosure glyph everywhere: a › that rotates down when open.
3647
+ + '.tacit-chevron{display:inline-block;font-size:12px;line-height:1;color:inherit;transition:transform .15s ease}.tacit-chevron-open{transform:rotate(90deg)}.tacit-card-chevron{margin-left:auto;color:var(--dsw-alias-label-secondary)}'
3648
+ + '.tacit-card-body{display:flex;flex-direction:column;gap:10px;padding:10px 12px 12px;border-top:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 60%, transparent)}.tacit-card-body[hidden]{display:none}'
3310
3649
  + '.tacit-usage{display:flex;flex-direction:column;gap:10px}'
3311
3650
  + '.tacit-tiles{display:flex;flex-wrap:wrap;gap:8px}'
3312
3651
  + '.tacit-tile{flex:1 1 140px;display:flex;flex-direction:column;gap:2px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px;min-width:0}'
@@ -3325,9 +3664,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3325
3664
  + '.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)}'
3326
3665
  + '.tacit-usage-runs{display:flex;flex-direction:column;gap:6px}'
3327
3666
  + '.tacit-usage-table{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden}'
3328
- + '.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}'
3667
+ + '.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}'
3329
3668
  + '.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)}'
3330
3669
  + '.tacit-usage-cell{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'
3670
+ // The cell is nowrap, so the in/out split needs its own block to sit under the total.
3671
+ + '.tacit-usage-tokens{display:flex;flex-direction:column}.tacit-usage-split{font-size:10px;color:var(--dsw-alias-label-secondary)}'
3331
3672
  + '.tacit-run-toggle{width:100%;text-align:left}'
3332
3673
  + '.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}'
3333
3674
  + '.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}'
@@ -3339,7 +3680,39 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3339
3680
  + '.tacit-pricing-table>.tacit-pricing-row:first-child{border-top:0}.tacit-pricing-head{color:var(--dsw-alias-label-secondary);font-weight:600;background:var(--dsw-alias-bg-layer-2)}'
3340
3681
  + '.tacit-pricing-cell{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tacit-pricing-model{font-weight:600}'
3341
3682
  + '.tacit-pricing-error{font-size:11px;color:var(--dsw-alias-state-warn-primary);line-height:1.5}'
3342
- + '@media (max-width:640px){.tacit-settings-label{min-width:0;flex-basis:100%}.tacit-modal-cols{grid-template-columns:1fr}.tacit-usage-row{display:flex;flex-direction:column}.tacit-usage-cell{white-space:normal}.tacit-pricing-row{display:flex;flex-direction:column}.tacit-pricing-cell{white-space:normal}.tacit-tile{flex-basis:45%}.tacit-breakdown-row{grid-template-columns:minmax(0,1fr) 4rem 5rem}}'
3683
+ // Directive cards: a status-coloured rail and pill badge, the text on
3684
+ // its own line, and a wrapping actions row, so nothing fights for width.
3685
+ + '.tacit-dir-list{display:flex;flex-direction:column;gap:8px}'
3686
+ + '.tacit-dir{--tacit-status:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--tacit-status);border-radius:8px;padding:8px 10px 8px 12px;display:flex;flex-direction:column;gap:6px;min-width:0}'
3687
+ + '.tacit-dir-active{--tacit-status:var(--dsw-alias-state-success-primary)}.tacit-dir-trial{--tacit-status:var(--dsw-alias-brand-primary)}.tacit-dir-retired{--tacit-status:var(--dsw-alias-state-warn-primary)}'
3688
+ + '.tacit-dir-head{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}.tacit-dir-start{margin-left:auto}'
3689
+ + '.tacit-badge{--tacit-badge:var(--tacit-status, var(--dsw-alias-label-secondary));display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px 1px 6px;font-size:11px;font-weight:600;line-height:1.5;color:var(--tacit-badge);background:color-mix(in srgb, var(--tacit-badge) 14%, transparent)}'
3690
+ + '.tacit-badge-active{--tacit-status:var(--dsw-alias-state-success-primary)}.tacit-badge-trial{--tacit-status:var(--dsw-alias-brand-primary)}.tacit-badge-retired{--tacit-status:var(--dsw-alias-state-warn-primary)}.tacit-badge-muted{--tacit-status:var(--dsw-alias-label-secondary)}'
3691
+ + '.tacit-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--tacit-badge);flex:none}'
3692
+ + '.tacit-dir-text{font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}.tacit-dir-off .tacit-dir-text{opacity:.5;text-decoration:line-through}'
3693
+ + '.tacit-dir-actions{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;font-size:11px;color:var(--dsw-alias-label-secondary)}'
3694
+ + '.tacit-dir-enabled{display:inline-flex;align-items:center;gap:5px;cursor:pointer}.tacit-dir-enabled .tacit-check{margin:0}.tacit-dir-remove{margin-left:auto}'
3695
+ + '.tacit-select-sm{font-size:11px;padding:2px 6px;width:auto}'
3696
+ + '.tacit-dir-receipt summary{cursor:pointer;font-size:11px;color:var(--dsw-alias-label-secondary)}.tacit-dir-receipt summary:hover{color:var(--dsw-alias-label-primary)}'
3697
+ + '.tacit-dir-add{display:flex;flex-wrap:wrap;align-items:center;gap:6px}'
3698
+ + '.tacit-options{display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px}'
3699
+ + '.tacit-option{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:1.5;cursor:pointer}.tacit-option .tacit-check{margin-top:2px}'
3700
+ // Hairlines between rows keep the group one soft box, not a list of boxes.
3701
+ + '.tacit-options>*+*{border-top:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 60%, transparent);padding-top:6px}.tacit-options .tacit-select,.tacit-options .tacit-input{background:var(--dsw-alias-bg-layer-1)}'
3702
+ // The receipt: tiles, a lifecycle timeline, trigger chips, then the
3703
+ // identity grid.
3704
+ + '.tacit-receipt{margin-top:8px;padding-top:10px;border-top:1px dashed var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:10px}'
3705
+ + '.tacit-receipt-tiles .tacit-tile{flex-basis:120px;background:var(--dsw-alias-bg-layer-1)}.tacit-receipt-tile-note{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.tacit-tile-muted{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}'
3706
+ + '.tacit-timeline{list-style:none;margin:0;padding:0;display:flex}'
3707
+ + '.tacit-timeline-step{flex:1;position:relative;min-width:0;padding:14px 6px 0 0;font-size:11px}'
3708
+ + '.tacit-timeline-step::before{content:"";position:absolute;left:0;top:0;width:9px;height:9px;border-radius:50%;box-sizing:border-box;background:var(--dsw-alias-brand-primary)}'
3709
+ + '.tacit-timeline-step::after{content:"";position:absolute;left:12px;right:2px;top:4px;height:1px;background:var(--dsw-alias-border-l1)}.tacit-timeline-step:last-child::after{display:none}'
3710
+ + '.tacit-timeline-never::before{background:var(--dsw-alias-bg-layer-2);border:1.5px solid var(--dsw-alias-border-l1)}.tacit-timeline-never .tacit-timeline-value{color:var(--dsw-alias-label-secondary);font-weight:400}'
3711
+ + '.tacit-timeline-label{display:block;color:var(--dsw-alias-label-secondary)}.tacit-timeline-value{display:block;font-weight:600;font-variant-numeric:tabular-nums}'
3712
+ + '.tacit-receipt-row{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}.tacit-receipt-label{font-size:11px;color:var(--dsw-alias-label-secondary);margin-right:2px}'
3713
+ + '.tacit-receipt-meta{margin:0;display:grid;grid-template-columns:max-content minmax(0,1fr);gap:3px 12px;font-size:11px}.tacit-receipt-meta dt{color:var(--dsw-alias-label-secondary)}.tacit-receipt-meta dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}'
3714
+ + '.tacit-receipt-actions{display:flex;justify-content:flex-end}'
3715
+ + '@media (max-width:640px){.tacit-field-label{flex-basis:100%}.tacit-field-control{margin-left:0}.tacit-modal-cols{grid-template-columns:1fr}.tacit-usage-row{display:flex;flex-direction:column}.tacit-usage-cell{white-space:normal}.tacit-pricing-row{display:flex;flex-direction:column}.tacit-pricing-cell{white-space:normal}.tacit-tile{flex-basis:45%}.tacit-breakdown-row{grid-template-columns:minmax(0,1fr) 4rem 5rem}}'
3343
3716
 
3344
3717
  function injectCss() {
3345
3718
  const tagId = 'dsh-tacit/styles.css'
@@ -3438,11 +3811,17 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3438
3811
  refreshPricing,
3439
3812
  openConfirm,
3440
3813
  closeConfirm,
3814
+ confirmAction,
3441
3815
  fetchBootstrapPreview,
3816
+ fetchReceipt,
3817
+ storeFor,
3818
+ analyzeTurn,
3819
+ coachSelected,
3442
3820
  clearAllRoot,
3443
3821
  clearUsageHistory,
3444
3822
  runNotice,
3445
3823
  fmtRate,
3824
+ fmtTokensSplit,
3446
3825
  ConfirmDialog,
3447
3826
  UsageCard,
3448
3827
  PricingCard,