dsh-tacit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -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,30 @@ 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.receiptTrialValue': '{turns} 轮,始于 {started} · 基线:杂乱 {messy}、纠正 {corrected}',
112
+ 'steer.receiptTriggers': '触发类别',
113
+ 'steer.receiptEvidence': '证据',
114
+ 'steer.receiptEvidenceValue': '{turns} 轮,来自 {conversations} 个会话',
115
+ 'steer.receiptCost': '提炼花费',
116
+ 'steer.receiptCostValue': '{usd} · {calls} 次调用',
117
+ 'steer.moveTo': '移到工作区',
118
+ 'steer.notSeen': '自 {date} 起未见',
92
119
  'settings.auto': '自动分析混乱轮次',
93
120
  'settings.budget': '每日自动分析上限',
94
121
  settings: '设置',
@@ -167,10 +194,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
167
194
  'usage.last30': '最近 30 天',
168
195
  'usage.since': '自 {day} 起',
169
196
  'usage.calls': '计费调用(30 天)',
197
+ 'usage.tokens': 'Tokens(30 天)',
170
198
  'usage.avgAnalysis': '单次分析中位成本',
171
199
  'usage.cachedRate': '输入缓存命中率(30 天)',
172
200
  'usage.unpriced': '无价目调用(30 天)',
173
201
  'usage.unpricedShort': '{n} 次无价目',
202
+ 'usage.failedRepair': '失败或修复调用花费(30 天)',
203
+ 'usage.failedRepairSplit': '失败 {failed} · 修复 {repair}',
204
+ 'usage.autoToday': '今日自动分析 {n} / {budget}',
174
205
  'usage.priceUnavailable': '无价目',
175
206
  'usage.chartLabel': '每日花费,最近 {n} 天',
176
207
  'usage.chartSummary': '最近 {n} 天:合计 {total};最高 {day} {max}',
@@ -178,6 +209,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
178
209
  'usage.series7': '7 天',
179
210
  'usage.series30': '30 天',
180
211
  'usage.breakdown': '按操作分类(最近 30 天)',
212
+ 'usage.byRoute': '按线路分类(最近 30 天)',
213
+ 'usage.byTrigger': '按触发方式分类(最近 30 天)',
181
214
  'usage.filters': '筛选',
182
215
  'usage.loading': '正在载入这次运行的调用明细…',
183
216
  'usage.page': '第 {page} / {pages} 页',
@@ -292,6 +325,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
292
325
  'err.network': '无法连接 Tacit 服务。',
293
326
  'err.internal': '内部错误:{detail}',
294
327
  'err.continuation': '这是一条纯粹的「继续」类消息(没有可分析的意图)——请分析它之前的那一轮。',
328
+ 'err.directive-policy': '指令不能改变工具权限、审批或沙箱设置,请换一种说法。',
329
+ 'err.already-analyzed': '这一轮已有报告;点击「重新分析」可再花一次费用分析。',
330
+ 'err.unknown-directive': '找不到这条指令。',
295
331
  }
296
332
 
297
333
  const DICT_EN = {
@@ -303,13 +339,16 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
303
339
  'status.trendCorrections': 'You corrected the agent: {a} → {b}',
304
340
  'status.trendMessy': 'Messy turns: {a} → {b}',
305
341
  'status.trendTokens': 'Tokens/turn: {a} → {b}',
306
- 'status.trendHint': '(first {n} turns vs. latest {n}, measured)',
342
+ 'status.trendHint': '(first {n} turns vs. latest {n}, measured — before/after on your own turns, not a controlled comparison)',
343
+ 'status.trendDocs': 'How these are judged: §6 Trials https://github.com/hackernotfound/dsh-tacit/blob/main/docs/how-it-works.md#6-trials · §10 The measured trend https://github.com/hackernotfound/dsh-tacit/blob/main/docs/how-it-works.md#10-the-measured-trend',
307
344
  'turn.enrichment': 'Context added before the send',
308
345
  'trigger.auto': 'auto',
309
346
  'trigger.correction': 'correction',
310
347
  'trigger.manual': 'manual',
311
348
  'trigger.bootstrap': 'bootstrap',
312
349
  'trigger.good': 'what worked',
350
+ 'trigger.feedback': 'feedback',
351
+ 'trigger.send': 'send',
313
352
  'report.strengths': 'What this prompt got right',
314
353
  'report.lesson': 'Lesson',
315
354
  'settings.learnGood': 'Also learn from a clean prompt right after a messy turn',
@@ -339,6 +378,30 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
339
378
  'steer.workspace': 'only {name}',
340
379
  'steer.scope': 'Applies to',
341
380
  'steer.everywhere': 'Everywhere',
381
+ 'steer.receipt': 'Where this directive came from',
382
+ 'steer.review': 'Review new directives before their trial',
383
+ 'steer.startTrial': 'Start trial',
384
+ 'steer.receiptLoading': 'Reading…',
385
+ 'steer.receiptCopy': 'Copy as JSON',
386
+ 'steer.receiptNever': 'never',
387
+ 'steer.receiptNoRun': 'no ledger run',
388
+ 'steer.receiptId': 'Id',
389
+ 'steer.receiptScope': 'Applies to',
390
+ 'steer.receiptStatus': 'Status',
391
+ 'steer.receiptSource': 'Source',
392
+ 'steer.receiptCreated': 'Created',
393
+ 'steer.receiptUpdated': 'Updated',
394
+ 'steer.receiptEvaluated': 'Evaluated',
395
+ 'steer.receiptVersion': 'Version',
396
+ 'steer.receiptTrial': 'Trial window',
397
+ 'steer.receiptTrialValue': '{turns} turns from {started} · baseline messy {messy}, corrections {corrected}',
398
+ 'steer.receiptTriggers': 'Trigger categories',
399
+ 'steer.receiptEvidence': 'Evidence',
400
+ 'steer.receiptEvidenceValue': '{turns} turns across {conversations} conversations',
401
+ 'steer.receiptCost': 'Distillation cost',
402
+ 'steer.receiptCostValue': '{usd} · {calls} calls',
403
+ 'steer.moveTo': 'Move to workspace',
404
+ 'steer.notSeen': 'not seen since {date}',
342
405
  'settings.auto': 'Auto-analyze messy turns',
343
406
  'settings.budget': 'Daily cap on automatic analyses',
344
407
  settings: 'Settings',
@@ -417,10 +480,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
417
480
  'usage.last30': 'Last 30 days',
418
481
  'usage.since': 'Since {day}',
419
482
  'usage.calls': 'Billed calls (30 d)',
483
+ 'usage.tokens': 'Tokens (30 d)',
420
484
  'usage.avgAnalysis': 'Median cost per analysis',
421
485
  'usage.cachedRate': 'Cached input rate (30 d)',
422
486
  'usage.unpriced': 'Unpriced calls (30 d)',
423
487
  'usage.unpricedShort': '{n} unpriced',
488
+ 'usage.failedRepair': 'Spent on failed or repair calls (30 d)',
489
+ 'usage.failedRepairSplit': 'failed {failed} · repair {repair}',
490
+ 'usage.autoToday': 'automatic analyses today {n} / {budget}',
424
491
  'usage.priceUnavailable': 'No price',
425
492
  'usage.chartLabel': 'Daily spend, last {n} days',
426
493
  'usage.chartSummary': 'Last {n} days: total {total}; highest {day} {max}',
@@ -428,6 +495,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
428
495
  'usage.series7': '7 d',
429
496
  'usage.series30': '30 d',
430
497
  'usage.breakdown': 'By operation (last 30 days)',
498
+ 'usage.byRoute': 'By route (last 30 days)',
499
+ 'usage.byTrigger': 'By trigger (last 30 days)',
431
500
  'usage.filters': 'Filters',
432
501
  'usage.loading': 'Loading this run\u2019s calls…',
433
502
  'usage.page': 'Page {page} of {pages}',
@@ -542,6 +611,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
542
611
  'err.network': 'Could not reach the Tacit service.',
543
612
  'err.internal': 'Internal error: {detail}',
544
613
  'err.continuation': 'This is a bare continuation ("continue", "go ahead") with nothing to analyze — analyze the turn before it instead.',
614
+ 'err.directive-policy': 'A directive cannot change tool permissions, approvals or the sandbox. Reword it.',
615
+ 'err.already-analyzed': 'This turn already has a report; Re-analyze pays for a fresh one.',
616
+ 'err.unknown-directive': 'No directive with that id.',
545
617
  }
546
618
 
547
619
  // ── API client (the host half's own routes on the harness origin) ──────
@@ -641,6 +713,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
641
713
  auto: null,
642
714
  steering: null, // {enabled, text}
643
715
  workspaces: [], // [{cwd, label}] from /state
716
+ receipts: {}, // directive id → the /directive-receipt receipt, re-read on each expand
644
717
  trend: null, // measured early-vs-recent trend
645
718
  bootstrap: null, // {running, done, total}
646
719
  coached: [], // cross-session coached-prompt entries
@@ -1029,13 +1102,13 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1029
1102
  function closeConfirm() {
1030
1103
  rootStore.confirm = null
1031
1104
  notifyRoot()
1105
+ restoreConfirmOpener()
1032
1106
  }
1033
1107
 
1034
1108
  /** Run the parked destructive action, whichever it is, and close the dialog. */
1035
1109
  async function confirmAction() {
1036
1110
  const kind = rootStore.confirm !== null && typeof rootStore.confirm === 'object' ? rootStore.confirm.kind : null
1037
- rootStore.confirm = null
1038
- notifyRoot()
1111
+ closeConfirm()
1039
1112
  if (kind === 'reports') await clearAllRoot()
1040
1113
  else if (kind === 'usage') await clearUsageHistory()
1041
1114
  }
@@ -1151,6 +1224,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1151
1224
  notifyRoot()
1152
1225
  }
1153
1226
 
1227
+ /** One directive's audit receipt. Re-read on every expand: a trial's counters move while it runs. */
1228
+ async function fetchReceipt(id) {
1229
+ const key = String(id)
1230
+ if (key.length === 0) return
1231
+ rootStore.error = null
1232
+ try {
1233
+ const result = await api('/directive-receipt', { id: key })
1234
+ if (result !== null && typeof result === 'object' && result.ok === true
1235
+ && result.receipt !== null && typeof result.receipt === 'object') {
1236
+ rootStore.receipts[key] = result.receipt
1237
+ } else {
1238
+ rootStore.error = { code: result !== null && typeof result === 'object' && typeof result.code === 'string' ? result.code : 'bad-request', detail: '' }
1239
+ }
1240
+ } catch (error) {
1241
+ rootStore.error = errorOf(error)
1242
+ }
1243
+ notifyRoot()
1244
+ }
1245
+
1154
1246
  async function initStore(store) {
1155
1247
  if (store === null || store.initStarted) return
1156
1248
  store.initStarted = true
@@ -1181,7 +1273,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1181
1273
  store.error = null
1182
1274
  store.notice = null
1183
1275
  notify(store)
1184
- api('/analyze', { sessionId: store.sessionId, turn })
1276
+ // A turn that already has a report is only re-analyzed on purpose; the button reads "Re-analyze" then.
1277
+ api('/analyze', { sessionId: store.sessionId, turn, ...(store.reports[String(turn)] ? { force: true } : {}) })
1185
1278
  .then((result) => {
1186
1279
  if (result !== null && typeof result === 'object' && result.ok && result.report !== null && typeof result.report === 'object') {
1187
1280
  store.reports[String(turn)] = result.report
@@ -1245,7 +1338,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1245
1338
  // `busy` means an analysis of that turn was already running — a
1246
1339
  // race with the auto-analyzer, not a failure of this batch.
1247
1340
  const code = typeof entry.code === 'string' && entry.code.length > 0 ? entry.code : 'call-failed'
1248
- if (failure === null && code !== 'busy') failure = { code, detail: '' }
1341
+ if (failure === null && code !== 'busy' && code !== 'already-analyzed') failure = { code, detail: '' }
1249
1342
  }
1250
1343
  if (failure !== null) store.error = failure
1251
1344
  if (result.profile !== null && typeof result.profile === 'object') store.profile = result.profile
@@ -1575,14 +1668,30 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1575
1668
  }
1576
1669
 
1577
1670
  /**
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.
1671
+ * Billed tokens in one bucket set, split the way they are priced:
1672
+ * everything the prompt cost (uncached input, cache reads, cache writes)
1673
+ * against what came back. `reasoningTokens` is a subset of `outputTokens`,
1674
+ * so adding it would count every reasoning token twice.
1581
1675
  */
1582
- function tokensTotal(buckets) {
1583
- if (buckets === null || typeof buckets !== 'object') return 0
1676
+ function tokensInOut(buckets) {
1677
+ if (buckets === null || typeof buckets !== 'object') return { input: 0, output: 0 }
1584
1678
  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)
1679
+ return {
1680
+ input: num(buckets.inputTokens) + num(buckets.cacheReadTokens) + num(buckets.cacheWriteTokens),
1681
+ output: num(buckets.outputTokens),
1682
+ }
1683
+ }
1684
+
1685
+ /** Both halves of `tokensInOut` at once. */
1686
+ function tokensTotal(buckets) {
1687
+ const split = tokensInOut(buckets)
1688
+ return split.input + split.output
1689
+ }
1690
+
1691
+ /** `tokensInOut` as the one line the tiles and the run rows both show. */
1692
+ function fmtTokensSplit(buckets) {
1693
+ const split = tokensInOut(buckets)
1694
+ return 'in ' + fmtTokens(split.input) + ' · out ' + fmtTokens(split.output)
1586
1695
  }
1587
1696
 
1588
1697
  function usageNum(value) {
@@ -1600,7 +1709,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1600
1709
  }
1601
1710
  }
1602
1711
 
1603
- /** One period card, zero-filled; the two derived figures stay nullable. */
1712
+ /**
1713
+ * One period card, zero-filled; the two averages stay nullable.
1714
+ * `failedUsd` and `repairUsd` overlap on a failed repair, so they can sum
1715
+ * past `failedOrRepairUsd`, which counts that union once.
1716
+ */
1604
1717
  function usagePeriod(value) {
1605
1718
  const source = value !== null && typeof value === 'object' ? value : {}
1606
1719
  return {
@@ -1608,8 +1721,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1608
1721
  billedCalls: usageNum(source.billedCalls),
1609
1722
  unmeteredCalls: usageNum(source.unmeteredCalls),
1610
1723
  unpricedCalls: usageNum(source.unpricedCalls),
1724
+ failedCalls: usageNum(source.failedCalls),
1611
1725
  tokens: usageTokens(source.tokens),
1612
1726
  usdKnown: usageNum(source.usdKnown),
1727
+ failedUsd: usageNum(source.failedUsd),
1728
+ repairUsd: usageNum(source.repairUsd),
1729
+ failedOrRepairUsd: usageNum(source.failedOrRepairUsd),
1613
1730
  avgAnalysisUsd: typeof source.avgAnalysisUsd === 'number' && Number.isFinite(source.avgAnalysisUsd) ? source.avgAnalysisUsd : null,
1614
1731
  cachedInputRate: typeof source.cachedInputRate === 'number' && Number.isFinite(source.cachedInputRate) ? source.cachedInputRate : null,
1615
1732
  }
@@ -1646,8 +1763,10 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1646
1763
  billedCalls: usageNum(source.billedCalls),
1647
1764
  unmeteredCalls: usageNum(source.unmeteredCalls),
1648
1765
  unpricedCalls: usageNum(source.unpricedCalls),
1766
+ failedCalls: usageNum(source.failedCalls),
1649
1767
  tokens: usageTokens(source.tokens),
1650
1768
  usdKnown: usageNum(source.usdKnown),
1769
+ failedUsd: usageNum(source.failedUsd),
1651
1770
  trigger: text('trigger'),
1652
1771
  startedAt: usageNum(source.startedAt),
1653
1772
  endedAt: usageNum(source.endedAt),
@@ -1668,6 +1787,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1668
1787
  if (value === null || typeof value !== 'object' || value.ok !== true) return null
1669
1788
  const runs = value.runs !== null && typeof value.runs === 'object' ? value.runs : {}
1670
1789
  const warnings = value.warnings !== null && typeof value.warnings === 'object' ? value.warnings : {}
1790
+ const auto = value.auto !== null && typeof value.auto === 'object' ? value.auto : {}
1671
1791
  return {
1672
1792
  trackingSince: usageNum(value.trackingSince),
1673
1793
  pricing: value.pricing !== null && typeof value.pricing === 'object' ? value.pricing : {},
@@ -1678,9 +1798,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1678
1798
  lifetime: usagePeriod(value.lifetime),
1679
1799
  byType: usageBuckets(value.byType),
1680
1800
  byModel: usageBuckets(value.byModel),
1801
+ byProvider: usageBuckets(value.byProvider),
1802
+ byTrigger: usageBuckets(value.byTrigger),
1681
1803
  series7: usageSeries(value.series7),
1682
1804
  series30: usageSeries(value.series30),
1683
1805
  warnings: { daily: usageWarning(warnings.daily), monthly: usageWarning(warnings.monthly) },
1806
+ auto: { today: usageNum(auto.today), budget: usageNum(auto.budget) },
1684
1807
  runs: {
1685
1808
  items: (Array.isArray(runs.items) ? runs.items : []).map(usageRunItem),
1686
1809
  page: usageNum(runs.page) > 0 ? Math.floor(usageNum(runs.page)) : 1,
@@ -1758,7 +1881,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
1758
1881
  h('span', { className: 'tacit-progress-count' }, autoOn ? t('status.autoOn', { today: String(today), budget: String(budget) }) : '')),
1759
1882
  h('div', { className: 'tacit-progress-text' }, autoOn ? t('status.autoHint') : t('status.autoOff')),
1760
1883
  hasTrend
1761
- ? h('div', { className: 'tacit-trend' },
1884
+ ? h('div', { className: 'tacit-trend', title: t('status.trendDocs') },
1762
1885
  h('span', { className: 'tacit-chip' }, t('status.trendCorrections', { a: pct(trend.early.correctionRate), b: pct(trend.recent.correctionRate) })),
1763
1886
  h('span', { className: 'tacit-chip' }, t('status.trendMessy', { a: pct(trend.early.messyRate), b: pct(trend.recent.messyRate) })),
1764
1887
  h('span', { className: 'tacit-chip' }, t('status.trendTokens', { a: fmt(trend.early.tokensPerTurn), b: fmt(trend.recent.tokensPerTurn) })),
@@ -2257,7 +2380,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2257
2380
  *
2258
2381
  * It is captured by the store action that opens the dialog rather than by
2259
2382
  * 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.
2383
+ * commits, so by the time the dialog is mounted the opener is already gone.
2261
2384
  */
2262
2385
  let confirmOpener = null
2263
2386
 
@@ -2272,26 +2395,29 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2272
2395
  confirmOpener = typeof document !== 'undefined' && document !== null ? document.activeElement : null
2273
2396
  }
2274
2397
 
2398
+ /**
2399
+ * Hand focus back one macrotask after the close. The host's Settings modal
2400
+ * moves focus on the same Escape, so a restore that runs inside the close is
2401
+ * overwritten and the user lands on the host's Close button instead.
2402
+ */
2403
+ function restoreConfirmOpener() {
2404
+ const opener = confirmOpener
2405
+ confirmOpener = null
2406
+ if (opener === null || typeof opener.focus !== 'function') return
2407
+ setTimeout(() => opener.focus(), 0)
2408
+ }
2409
+
2275
2410
  /**
2276
2411
  * One small modal confirmation, rendered unconditionally by its owner (and
2277
2412
  * `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.
2413
+ * Hook-free: focus is restored by `closeConfirm`, the one path out.
2280
2414
  *
2281
2415
  * Cancel comes first and takes focus: the safe answer is the one a keyboard
2282
2416
  * or screen-reader user reaches without looking.
2283
2417
  */
2284
2418
  function ConfirmDialog(kit, { open, title, body, confirmLabel, onConfirm, onCancel }) {
2285
2419
  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
2420
+ if (open !== true) return null
2295
2421
  const cancel = typeof onCancel === 'function' ? onCancel : () => {}
2296
2422
  return h('div', {
2297
2423
  className: 'tacit-modal-backdrop',
@@ -2336,9 +2462,68 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2336
2462
 
2337
2463
  function DirectivesEditor(kit) {
2338
2464
  const { t } = kit
2465
+ const fmtDay = (ms) => {
2466
+ const date = new Date(Number(ms))
2467
+ return !(Number(ms) > 0) || Number.isNaN(date.getTime()) ? t('steer.receiptNever') : date.toISOString().slice(0, 10)
2468
+ }
2469
+ const pct = (value) => (typeof value === 'number' && value >= 0 ? Math.round(value * 100) + '%' : t('steer.receiptNever'))
2470
+ const triggerName = (name) => {
2471
+ const key = 'trigger.' + name
2472
+ const label = t(key)
2473
+ return label === key ? name : label
2474
+ }
2475
+ const copyReceipt = (receipt) => {
2476
+ if (typeof navigator === 'undefined' || navigator === null || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return
2477
+ navigator.clipboard.writeText(JSON.stringify(receipt, null, 2)).catch(() => {})
2478
+ }
2479
+ const receiptRow = (label, value) => [h('dt', { key: label + '-t' }, label), h('dd', { key: label + '-d' }, value)]
2480
+ /** What the receipt route returned for this directive, as definition rows; never any prompt text. */
2481
+ function Receipt(receipt) {
2482
+ if (receipt === null || typeof receipt === 'object' === false) return h('div', { className: 'tacit-panel-hint' }, t('steer.receiptLoading'))
2483
+ const triggers = Object.entries(receipt.triggers !== null && typeof receipt.triggers === 'object' ? receipt.triggers : {})
2484
+ .map(([name, count]) => triggerName(name) + ' ' + String(count)).join(' · ')
2485
+ const evidence = receipt.evidence !== null && typeof receipt.evidence === 'object' ? receipt.evidence : { turns: 0, conversations: 0 }
2486
+ const cost = receipt.cost !== null && typeof receipt.cost === 'object' ? receipt.cost : { usd: null, calls: 0 }
2487
+ const trial = receipt.trial !== null && typeof receipt.trial === 'object' ? receipt.trial : null
2488
+ return h('div', null,
2489
+ h('dl', { className: 'tacit-receipt' },
2490
+ ...receiptRow(t('steer.receiptId'), String(receipt.id)),
2491
+ ...receiptRow(t('steer.receiptScope'), typeof receipt.scope === 'string' && receipt.scope.length > 0 ? receipt.scope : t('steer.everywhere')),
2492
+ ...receiptRow(t('steer.receiptStatus'), String(receipt.status)),
2493
+ ...receiptRow(t('steer.receiptSource'), receipt.source === 'user' ? t('steer.user') : t('steer.distilled')),
2494
+ ...receiptRow(t('steer.receiptCreated'), fmtDay(receipt.createdAt)),
2495
+ ...receiptRow(t('steer.receiptUpdated'), fmtDay(receipt.updatedAt)),
2496
+ ...receiptRow(t('steer.receiptEvaluated'), fmtDay(receipt.evaluatedAt)),
2497
+ ...receiptRow(t('steer.receiptVersion'), String(receipt.version)),
2498
+ ...receiptRow(t('steer.receiptTrial'), trial === null ? t('steer.receiptNever') : t('steer.receiptTrialValue', {
2499
+ turns: String(trial.turns), started: fmtDay(trial.startedAt), messy: pct(trial.baselineMessyRate), corrected: pct(trial.baselineCorrectionRate) })),
2500
+ ...receiptRow(t('steer.receiptTriggers'), triggers.length > 0 ? triggers : t('steer.receiptNever')),
2501
+ ...receiptRow(t('steer.receiptEvidence'), t('steer.receiptEvidenceValue', { turns: String(evidence.turns), conversations: String(evidence.conversations) })),
2502
+ ...receiptRow(t('steer.receiptCost'), cost.usd === null ? t('steer.receiptNoRun') : t('steer.receiptCostValue', { usd: fmtUsd(cost.usd), calls: String(cost.calls) }))),
2503
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => copyReceipt(receipt) }, t('steer.receiptCopy')))
2504
+ }
2339
2505
  return function DirectivesEditorView(props) {
2340
- const { config, directives, steering } = props
2506
+ const { config, steering } = props
2507
+ const directives = (Array.isArray(props.directives) ? props.directives : []).filter((entry) => entry.status !== 'removed')
2508
+ const receipts = rootStore.receipts !== null && typeof rootStore.receipts === 'object' ? rootStore.receipts : {}
2509
+ const reviewOn = config !== null && config.reviewCandidates === true
2341
2510
  const workspaces = Array.isArray(props.workspaces) ? props.workspaces : []
2511
+ const seenAt = props.seenAt !== null && typeof props.seenAt === 'object' ? props.seenAt : {}
2512
+ const liveScope = (scope) => workspaces.some((entry) => entry.cwd === scope || String(entry.cwd).startsWith(scope + '/'))
2513
+ const notSeenSince = (entry) => {
2514
+ const stamp = typeof seenAt[entry.workspace] === 'number' ? seenAt[entry.workspace] : entry.createdAt
2515
+ const date = new Date(Number(stamp))
2516
+ return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
2517
+ }
2518
+ const scopeOptions = (current) => {
2519
+ const options = [
2520
+ h('option', { key: '-', value: '-' }, t('steer.moveTo')),
2521
+ h('option', { key: '', value: '' }, t('steer.everywhere')),
2522
+ ]
2523
+ for (const entry of workspaces) options.push(h('option', { key: entry.cwd, value: entry.cwd }, entry.label))
2524
+ if (current.length > 0 && !workspaces.some((entry) => entry.cwd === current)) options.push(h('option', { key: current, value: current }, labelOf(current)))
2525
+ return options
2526
+ }
2342
2527
  const [draft, setDraft] = useState('')
2343
2528
  const [scope, setScope] = useState('')
2344
2529
  const labelOf = (cwd) => {
@@ -2361,6 +2546,7 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2361
2546
  setEnrichOn(next)
2362
2547
  updateRootConfig({ enrichPrompts: next })
2363
2548
  }
2549
+ const toggleReview = () => updateRootConfig({ reviewCandidates: !reviewOn })
2364
2550
  const onAdd = () => {
2365
2551
  const text = draft.trim()
2366
2552
  if (text.length === 0) return
@@ -2376,10 +2562,14 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2376
2562
  h('div', { className: 'tacit-settings-row' },
2377
2563
  h('label', { className: 'tacit-settings-label' }, t('steer.enrich')),
2378
2564
  h('input', { type: 'checkbox', checked: enrichOn, onChange: toggleEnrich })),
2565
+ h('div', { className: 'tacit-settings-row' },
2566
+ h('label', { className: 'tacit-settings-label' }, t('steer.review')),
2567
+ h('input', { type: 'checkbox', checked: reviewOn, onChange: toggleReview })),
2379
2568
  directives.length === 0
2380
2569
  ? h('div', { className: 'tacit-empty' }, t('steer.empty'))
2381
2570
  : 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' : '') },
2571
+ directives.map((entry) => h('div', { key: entry.id, className: 'tacit-directive-block' },
2572
+ h('div', { className: 'tacit-rule tacit-directive' + (entry.enabled === false ? ' tacit-directive-off' : '') },
2383
2573
  h('input', {
2384
2574
  type: 'checkbox',
2385
2575
  className: 'tacit-check',
@@ -2392,6 +2582,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2392
2582
  typeof entry.workspace === 'string' && entry.workspace.length > 0
2393
2583
  ? h('span', { className: 'tacit-chip tacit-chip-scope', title: entry.workspace }, t('steer.workspace', { name: labelOf(entry.workspace) }))
2394
2584
  : null,
2585
+ typeof entry.workspace === 'string' && entry.workspace.length > 0 && !liveScope(entry.workspace)
2586
+ ? h('span', { className: 'tacit-chip tacit-chip-muted', title: entry.workspace }, t('steer.notSeen', { date: notSeenSince(entry) }))
2587
+ : null,
2395
2588
  entry.status === 'queued'
2396
2589
  ? h('span', { className: 'tacit-chip tacit-chip-muted' }, t('steer.queued'))
2397
2590
  : entry.status === 'candidate'
@@ -2402,7 +2595,27 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2402
2595
  : entry.status === 'retired'
2403
2596
  ? h('span', { className: 'tacit-chip tacit-chip-warn' }, t('steer.retired', { reason: String(entry.retiredReason || '') }))
2404
2597
  : 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'))))),
2598
+ reviewOn && entry.status === 'queued' && !(entry.approvedAt > 0)
2599
+ ? h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'start-trial', id: entry.id }) }, t('steer.startTrial'))
2600
+ : null,
2601
+ h('select', {
2602
+ className: 'tacit-input tacit-select tacit-select-sm',
2603
+ 'aria-label': t('steer.moveTo'),
2604
+ onChange: (event) => {
2605
+ if (event.target.value !== '-') editDirectives({ action: 'rescope', id: entry.id, workspace: event.target.value })
2606
+ },
2607
+ }, ...scopeOptions(typeof entry.workspace === 'string' ? entry.workspace : '')),
2608
+ h('button', { type: 'button', className: 'tacit-btn tacit-btn-sm', onClick: () => editDirectives({ action: 'remove', id: entry.id }) }, t('steer.remove'))),
2609
+ h('details', {
2610
+ className: 'tacit-preview',
2611
+ 'data-testid': 'tacit-receipt',
2612
+ 'data-directive-id': entry.id,
2613
+ onToggle: (event) => {
2614
+ if (event.target.open) fetchReceipt(entry.id)
2615
+ },
2616
+ },
2617
+ h('summary', null, t('steer.receipt')),
2618
+ Receipt(receipts[entry.id] ?? null))))),
2406
2619
  h('div', { className: 'tacit-settings-row' },
2407
2620
  h('input', {
2408
2621
  className: 'tacit-input tacit-directive-input',
@@ -2655,7 +2868,8 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2655
2868
  ]),
2656
2869
  card('guidance', [
2657
2870
  h(DirectivesEditorView, {
2658
- workspaces: rootStore.workspaces, config, directives, steering: rootStore.steering }),
2871
+ workspaces: rootStore.workspaces, config, directives, steering: rootStore.steering,
2872
+ seenAt: rootStore.profile !== null && typeof rootStore.profile === 'object' ? rootStore.profile.workspaceSeenAt : null }),
2659
2873
  ]),
2660
2874
  card('improve', [
2661
2875
  h('div', { className: 'tacit-settings-row' },
@@ -2792,10 +3006,12 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2792
3006
  typeof note === 'string' && note.length > 0 ? h('span', { className: 'tacit-tile-note' }, note) : null)
2793
3007
  }
2794
3008
 
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)
3009
+ /** Money tile: the period's spend, its unpriced-call footnote, and any second note that period carries. */
3010
+ function SpendTile(kit, id, label, totals, extraNote) {
3011
+ const notes = []
3012
+ if (totals.unpricedCalls > 0) notes.push(kit.t('usage.unpricedShort', { n: String(totals.unpricedCalls) }))
3013
+ if (typeof extraNote === 'string' && extraNote.length > 0) notes.push(extraNote)
3014
+ return UsageTile(id, label, usageSpend(kit, totals), notes.join(' · '))
2799
3015
  }
2800
3016
 
2801
3017
  /**
@@ -2858,18 +3074,33 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
2858
3074
  seriesButton('30', t('usage.series30'))))
2859
3075
  }
2860
3076
 
2861
- /** Spend per operation, costliest first, each row ending in a share bar. */
2862
- function UsageBreakdown(kit, { byType }) {
3077
+ /**
3078
+ * `trigger` is a free string on the wire, so a host that starts tagging
3079
+ * runs with something this dictionary has never heard of shows the tag
3080
+ * itself rather than a raw `trigger.<key>` lookup miss.
3081
+ */
3082
+ function triggerLabel(t, trigger) {
3083
+ const key = 'trigger.' + trigger
3084
+ const label = t(key)
3085
+ return label === key ? trigger : label
3086
+ }
3087
+
3088
+ /**
3089
+ * Spend across one bucket record, costliest first, each row ending in a
3090
+ * share bar. `label` turns a bucket key into its display name, which is a
3091
+ * dictionary lookup for the closed sets and the raw key for the open ones.
3092
+ */
3093
+ function UsageBreakdown(kit, { buckets, title, label }) {
2863
3094
  const { t } = kit
2864
- const rows = Object.entries(byType).sort((a, b) => b[1].usdKnown - a[1].usdKnown)
3095
+ const rows = Object.entries(buckets).sort((a, b) => b[1].usdKnown - a[1].usdKnown)
2865
3096
  if (rows.length === 0) return null
2866
3097
  const top = rows[0][1].usdKnown
2867
- return h('div', { className: 'tacit-usage-breakdown' },
2868
- h('div', { className: 'tacit-report-title' }, t('usage.breakdown')),
3098
+ return h('div', { key: title, className: 'tacit-usage-breakdown' },
3099
+ h('div', { className: 'tacit-report-title' }, t(title)),
2869
3100
  rows.map(([type, totals]) => {
2870
3101
  const share = top > 0 ? Math.round((totals.usdKnown / top) * 100) : 0
2871
3102
  return h('div', { key: type, className: 'tacit-breakdown-row' },
2872
- h('span', { className: 'tacit-breakdown-name' }, t('runtype.' + type)),
3103
+ h('span', { className: 'tacit-breakdown-name' }, label(type)),
2873
3104
  h('span', { className: 'tacit-breakdown-calls' }, fmtTokens(totals.billedCalls)),
2874
3105
  h('span', { className: 'tacit-breakdown-tokens' }, fmtTokens(tokensTotal(totals.tokens))),
2875
3106
  h('span', { className: 'tacit-breakdown-usd' }, usageSpend(kit, totals)),
@@ -3009,7 +3240,9 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3009
3240
  cell('model', item.model.length > 0 ? item.model : '—'),
3010
3241
  cell('status', h('span', { className: 'tacit-chip tacit-status-' + item.status }, t('status.' + item.status))),
3011
3242
  cell('calls', fmtTokens(item.billedCalls)),
3012
- cell('tokens', fmtTokens(tokensTotal(item.tokens))),
3243
+ cell('tokens', h('span', { className: 'tacit-usage-tokens' },
3244
+ h('span', null, fmtTokens(tokensTotal(item.tokens))),
3245
+ h('span', { className: 'tacit-usage-split' }, fmtTokensSplit(item.tokens)))),
3013
3246
  cell('cost', usageSpend(kit, item)))
3014
3247
  }
3015
3248
 
@@ -3070,19 +3303,25 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3070
3303
  return h('div', { className: 'tacit-usage' },
3071
3304
  h('p', { className: 'tacit-panel-hint' }, t('usage.label')),
3072
3305
  h('div', { className: 'tacit-tiles' },
3073
- SpendTile(kit, 'today', t('usage.today'), usage.today),
3306
+ SpendTile(kit, 'today', t('usage.today'), usage.today,
3307
+ t('usage.autoToday', { n: String(usage.auto.today), budget: String(usage.auto.budget) })),
3074
3308
  SpendTile(kit, 'month', t('usage.month'), usage.month),
3075
3309
  SpendTile(kit, 'last30', t('usage.last30'), last30),
3076
3310
  SpendTile(kit, 'lifetime', t('usage.since', { day: since }), usage.lifetime)),
3077
3311
  h('div', { className: 'tacit-tiles' },
3078
3312
  UsageTile('calls', t('usage.calls'), fmtTokens(last30.billedCalls)),
3313
+ UsageTile('tokens', t('usage.tokens'), fmtTokensSplit(last30.tokens)),
3079
3314
  UsageTile('avg', t('usage.avgAnalysis'), last30.avgAnalysisUsd === null
3080
3315
  ? t('usage.priceUnavailable')
3081
3316
  : fmtUsd(last30.avgAnalysisUsd)),
3082
3317
  UsageTile('cached', t('usage.cachedRate'), fmtPct(last30.cachedInputRate)),
3083
- UsageTile('unpriced', t('usage.unpriced'), fmtTokens(last30.unpricedCalls))),
3318
+ UsageTile('unpriced', t('usage.unpriced'), fmtTokens(last30.unpricedCalls)),
3319
+ UsageTile('failedRepair', t('usage.failedRepair'), fmtUsd(last30.failedOrRepairUsd),
3320
+ t('usage.failedRepairSplit', { failed: fmtUsd(last30.failedUsd), repair: fmtUsd(last30.repairUsd) }))),
3084
3321
  BarStrip(kit, { series: which === '7' ? usage.series7 : usage.series30, which, onSeries: props.onSeries }),
3085
- UsageBreakdown(kit, { byType: usage.byType }),
3322
+ UsageBreakdown(kit, { buckets: usage.byType, title: 'usage.breakdown', label: (type) => t('runtype.' + type) }),
3323
+ UsageBreakdown(kit, { buckets: usage.byProvider, title: 'usage.byRoute', label: (provider) => provider }),
3324
+ UsageBreakdown(kit, { buckets: usage.byTrigger, title: 'usage.byTrigger', label: (trigger) => triggerLabel(t, trigger) }),
3086
3325
  WarnBar(kit, { id: 'daily', warning: usage.warnings.daily }),
3087
3326
  WarnBar(kit, { id: 'monthly', warning: usage.warnings.monthly }),
3088
3327
  UsageFilters(kit, {
@@ -3325,9 +3564,11 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3325
3564
  + '.tacit-usage-filters{display:flex;flex-direction:column;gap:6px}.tacit-filter-row{display:flex;flex-wrap:wrap;gap:8px}.tacit-filter{display:flex;flex-direction:column;gap:2px;min-width:0}.tacit-filter-label{font-size:10px;color:var(--dsw-alias-label-secondary)}'
3326
3565
  + '.tacit-usage-runs{display:flex;flex-direction:column;gap:6px}'
3327
3566
  + '.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}'
3567
+ + '.tacit-usage-row{display:grid;grid-template-columns:minmax(90px,1fr) minmax(90px,1.2fr) minmax(80px,1.2fr) minmax(90px,1.2fr) 5rem 4rem 7rem 5rem;gap:6px;align-items:center;padding:5px 8px;border-top:1px solid var(--dsw-alias-border-l1);font-size:11px;font-variant-numeric:tabular-nums}'
3329
3568
  + '.tacit-usage-table>.tacit-usage-row:first-child{border-top:0}.tacit-usage-head{color:var(--dsw-alias-label-secondary);font-weight:600;background:var(--dsw-alias-bg-layer-2)}'
3330
3569
  + '.tacit-usage-cell{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'
3570
+ // The cell is nowrap, so the in/out split needs its own block to sit under the total.
3571
+ + '.tacit-usage-tokens{display:flex;flex-direction:column}.tacit-usage-split{font-size:10px;color:var(--dsw-alias-label-secondary)}'
3331
3572
  + '.tacit-run-toggle{width:100%;text-align:left}'
3332
3573
  + '.tacit-usage-attempts{border-top:1px dashed var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);padding:6px 8px}.tacit-usage-attempts-cell{display:flex;flex-direction:column;gap:4px}'
3333
3574
  + '.tacit-attempt{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-primary)}.tacit-attempt-meta,.tacit-attempt-tokens{color:var(--dsw-alias-label-secondary)}.tacit-attempt-usd{margin-left:auto;font-variant-numeric:tabular-nums}'
@@ -3438,11 +3679,17 @@ if (typeof window === 'undefined' || window.__ModuleLoader__ === undefined || ty
3438
3679
  refreshPricing,
3439
3680
  openConfirm,
3440
3681
  closeConfirm,
3682
+ confirmAction,
3441
3683
  fetchBootstrapPreview,
3684
+ fetchReceipt,
3685
+ storeFor,
3686
+ analyzeTurn,
3687
+ coachSelected,
3442
3688
  clearAllRoot,
3443
3689
  clearUsageHistory,
3444
3690
  runNotice,
3445
3691
  fmtRate,
3692
+ fmtTokensSplit,
3446
3693
  ConfirmDialog,
3447
3694
  UsageCard,
3448
3695
  PricingCard,