@kky42/pievo-server 2.3.0 → 2.4.2

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.
Files changed (199) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/MachinesModal-CSbnrUER.js +1 -0
  3. package/.output/public/assets/SignIn-0Tjqs-Ma.js +1 -0
  4. package/.output/public/assets/actionUi-DpwGIGpw.js +1 -0
  5. package/.output/public/assets/app-CaZpuwOS.css +2 -0
  6. package/.output/public/assets/index-CaS_SmDq.js +10 -0
  7. package/.output/public/assets/invite._token-Btg_0aLp.js +1 -0
  8. package/.output/public/assets/invite._token-KMD2JAgE.js +1 -0
  9. package/.output/public/assets/lifecycleUi-C7dwHtbw.js +1 -0
  10. package/.output/public/assets/loops._loopId-D5usF3DO.js +67 -0
  11. package/.output/public/assets/loops._loopId_.runs._runId-DRCgXaCq.js +3 -0
  12. package/.output/public/assets/preload-helper-DsxECZIS.js +5 -0
  13. package/.output/public/assets/routes-CtFgwdgW.js +1 -0
  14. package/.output/public/assets/routes-LDbnnQ3f.js +1 -0
  15. package/.output/public/assets/t._teamId-D41xcbsF.js +1 -0
  16. package/.output/public/assets/t._teamId-DUena1pV.js +1 -0
  17. package/.output/public/assets/ui-Do4gqMyP.js +1 -0
  18. package/.output/server/_libs/@base-ui/react+[...].mjs +4130 -4130
  19. package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
  20. package/.output/server/_libs/drizzle-orm+postgres.mjs +28 -1
  21. package/.output/server/_ssr/DashboardView-C2LdV6J0.mjs +914 -0
  22. package/.output/server/_ssr/{notifyFns-DuPR1zd7.mjs → MachinesModal-BjkcVjjx.mjs} +55 -35
  23. package/.output/server/_ssr/{SignIn-RmpWm9uy.mjs → SignIn-DEGrvJvb.mjs} +10 -24
  24. package/.output/server/_ssr/SocialLinks-Cm4H8pUJ.mjs +1044 -0
  25. package/.output/server/_ssr/{actionUi-BN55TNYE.mjs → actionUi-DJBWuNwr.mjs} +3 -3
  26. package/.output/server/_ssr/{artifactFiles-BCaIIrrT.mjs → artifactFiles-DtpRidFZ.mjs} +7 -8
  27. package/.output/server/_ssr/{artifactKind-BW0isV8L.mjs → artifactKind-FBYbgSuf.mjs} +27 -2
  28. package/.output/server/_ssr/artifacts-BR3jj0NC.mjs +2 -0
  29. package/.output/server/_ssr/{auth-30Z4lV1w.mjs → auth-C0zoZCGe.mjs} +16 -19
  30. package/.output/server/_ssr/boot-CdOO4xOo.mjs +2 -0
  31. package/.output/server/_ssr/{boot-3BNKujyp.mjs → boot-sNlIkoLe.mjs} +1331 -3927
  32. package/.output/server/_ssr/{bun-sqlite-dialect-BW9W1_Ps-_7tfuNRi.mjs → bun-sqlite-dialect-BW9W1_Ps-DmlZWqO7.mjs} +2 -2
  33. package/.output/server/_ssr/{d1-sqlite-dialect-BLC8LXE6-ucDUbeBk.mjs → d1-sqlite-dialect-BLC8LXE6-CCNzqZwZ.mjs} +2 -2
  34. package/.output/server/_ssr/db-CwLD-5w1.mjs +2 -0
  35. package/.output/server/_ssr/{dist-Cd6nlLlO.mjs → dist-B3SxDOpW.mjs} +2 -2
  36. package/.output/server/_ssr/{factory-CWWwV7XZ.mjs → factory-Dv3Zopis.mjs} +2 -2
  37. package/.output/server/_ssr/invite._token-CJ6-H-s3.mjs +20 -0
  38. package/.output/server/_ssr/{invite._token-SdWXemIc.mjs → invite._token-dLKDJKVs.mjs} +9 -17
  39. package/.output/server/_ssr/{invite._token-BKlz0R2f.mjs → invite._token-l1SjouDz.mjs} +6 -15
  40. package/.output/server/_ssr/{kysely-adapter-C8iS51DB.mjs → kysely-adapter-nL7WKSgw.mjs} +1 -1
  41. package/.output/server/_ssr/{lifecycleUi-DgBQAYzp.mjs → lifecycleUi-Dad3hvhm.mjs} +11 -11
  42. package/.output/server/_ssr/logger-B8m8F1Qu.mjs +2 -0
  43. package/.output/server/_ssr/loopApi-BKyFB4KM.mjs +342 -0
  44. package/.output/server/_ssr/{adapters-BoLkOf1T.mjs → loopProjection-B8a829ij.mjs} +25 -55
  45. package/.output/server/_ssr/loops._loopId-DS16Phmq.mjs +1271 -0
  46. package/.output/server/_ssr/{loops._loopId_.runs._runId-PUGBrD5q.mjs → loops._loopId_.runs._runId-BEJI6iK5.mjs} +23 -74
  47. package/.output/server/_ssr/{loops._loopId_.runs._runId-Ymy4lwee.mjs → loops._loopId_.runs._runId-CduETLqH.mjs} +4 -5
  48. package/.output/server/_ssr/{machineFns-BZ9puxDt.mjs → machineFns-ByMsgG12.mjs} +3 -5
  49. package/.output/server/_ssr/{node-sqlite-dialect-D_3JmkMo.mjs → node-sqlite-dialect-CjNineno.mjs} +2 -2
  50. package/.output/server/_ssr/router-CXyrnvx-.mjs +565 -0
  51. package/.output/server/_ssr/{routes-ClZEdsnB.mjs → routes-BulNEUEE.mjs} +6 -10
  52. package/.output/server/_ssr/{routes-ComqHvCL.mjs → routes-DD94baie.mjs} +3 -3
  53. package/.output/server/_ssr/{routes-D5jJJ-30.mjs → routes-Wjexi35W.mjs} +6 -6
  54. package/.output/server/_ssr/runDiff-Ba8Lg1vg.mjs +2 -0
  55. package/.output/server/_ssr/ssr.mjs +60 -100
  56. package/.output/server/_ssr/store-Cujtg2QE.mjs +2 -0
  57. package/.output/server/_ssr/{t._teamId-BgHV_KFP.mjs → t._teamId-1CreIlPV.mjs} +7 -11
  58. package/.output/server/_ssr/{t._teamId-DDERNgfn.mjs → t._teamId-Bd6-zZsy.mjs} +4 -4
  59. package/.output/server/_ssr/{t._teamId-DRJvKHZn.mjs → t._teamId-COuMKQ4r.mjs} +7 -7
  60. package/.output/server/_ssr/{teamFns-Bp81UO-x.mjs → teamFns-H438WnZC.mjs} +12 -30
  61. package/.output/server/_ssr/{teamFns-bm82twd5.mjs → teamFns-j2JKjorZ.mjs} +2 -2
  62. package/.output/server/_ssr/tokens-DgG_Syq3.mjs +2 -0
  63. package/.output/server/_ssr/{ui-Ww2tTM2u.mjs → ui-D5KWstlb.mjs} +26 -48
  64. package/.output/server/{_tanstack-start-manifest_v-DwKENYCN.mjs → _tanstack-start-manifest_v-3H7yBKLz.mjs} +25 -35
  65. package/{drizzle/0000_round_marauders.sql → .output/server/drizzle/0000_baseline.sql} +102 -44
  66. package/.output/server/drizzle/meta/_journal.json +3 -115
  67. package/.output/server/index.mjs +83 -132
  68. package/{.output/server/drizzle/0000_round_marauders.sql → drizzle/0000_baseline.sql} +102 -44
  69. package/drizzle/meta/_journal.json +3 -115
  70. package/package.json +2 -11
  71. package/scripts/pievo-server.mjs +3 -1
  72. package/scripts/server-cli-lib.mjs +23 -16
  73. package/.output/public/assets/CodeField-dOzyFQNH.js +0 -34
  74. package/.output/public/assets/LoopView-DjVHUyrR.js +0 -59
  75. package/.output/public/assets/Modal-CCRvx959.js +0 -1
  76. package/.output/public/assets/SignIn-Qxw5zDFq.js +0 -1
  77. package/.output/public/assets/actionUi-CXxhzYe2.js +0 -1
  78. package/.output/public/assets/app-D1cij_NJ.css +0 -2
  79. package/.output/public/assets/artifactView-Cd5wWPil.js +0 -66
  80. package/.output/public/assets/index-BRN_CQ8U.js +0 -12
  81. package/.output/public/assets/invite._token-BIxr8DlP.js +0 -1
  82. package/.output/public/assets/invite._token-j2BTCeG1.js +0 -1
  83. package/.output/public/assets/jsx-runtime-BseJUIpC.js +0 -1
  84. package/.output/public/assets/lifecycleUi-Qy_8beQK.js +0 -1
  85. package/.output/public/assets/loopApi-CT1mMhlb.js +0 -3
  86. package/.output/public/assets/loops._loopId-BRBQzioy.js +0 -4
  87. package/.output/public/assets/loops._loopId_.runs._runId-CMnOJkem.js +0 -3
  88. package/.output/public/assets/notifyFns-DYD4EKaG.js +0 -1
  89. package/.output/public/assets/preload-helper-DFkkpP6W.js +0 -3
  90. package/.output/public/assets/routes-3nuEJGdo.js +0 -1
  91. package/.output/public/assets/routes-DORLim-C.js +0 -1
  92. package/.output/public/assets/t._teamId-F5f4K8kd.js +0 -1
  93. package/.output/public/assets/t._teamId-XfE_S1Fo.js +0 -1
  94. package/.output/public/assets/ui-Dhsx6SW0.js +0 -1
  95. package/.output/public/assets/useRouter-DsjH8G0p.js +0 -1
  96. package/.output/server/_libs/@codemirror/autocomplete+[...].mjs +0 -19713
  97. package/.output/server/_libs/@codemirror/lang-css+[...].mjs +0 -2636
  98. package/.output/server/_libs/@codemirror/lang-html+[...].mjs +0 -2247
  99. package/.output/server/_libs/@codemirror/lang-json+[...].mjs +0 -79
  100. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1736
  101. package/.output/server/_libs/@uiw/codemirror-extensions-basic-setup+[...].mjs +0 -76
  102. package/.output/server/_libs/babel__runtime.mjs +0 -30
  103. package/.output/server/_libs/clsx.mjs +0 -16
  104. package/.output/server/_libs/codemirror__commands.mjs +0 -1725
  105. package/.output/server/_libs/codemirror__lint.mjs +0 -565
  106. package/.output/server/_libs/codemirror__search.mjs +0 -1138
  107. package/.output/server/_libs/codemirror__theme-one-dark.mjs +0 -161
  108. package/.output/server/_libs/d3-array.mjs +0 -231
  109. package/.output/server/_libs/d3-color.mjs +0 -330
  110. package/.output/server/_libs/d3-format.mjs +0 -252
  111. package/.output/server/_libs/d3-interpolate.mjs +0 -163
  112. package/.output/server/_libs/d3-path.mjs +0 -77
  113. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  114. package/.output/server/_libs/d3-shape.mjs +0 -818
  115. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  116. package/.output/server/_libs/dom-serializer+[...].mjs +0 -708
  117. package/.output/server/_libs/domhandler.mjs +0 -461
  118. package/.output/server/_libs/domutils.mjs +0 -868
  119. package/.output/server/_libs/es-toolkit.mjs +0 -1737
  120. package/.output/server/_libs/eventemitter3.mjs +0 -236
  121. package/.output/server/_libs/html-dom-parser+htmlparser2.mjs +0 -1560
  122. package/.output/server/_libs/html-react-parser+[...].mjs +0 -1360
  123. package/.output/server/_libs/react-is.mjs +0 -48
  124. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  125. package/.output/server/_libs/recharts+victory-vendor.mjs +0 -17303
  126. package/.output/server/_libs/uiw__codemirror-themes.mjs +0 -32
  127. package/.output/server/_libs/uiw__react-codemirror.mjs +0 -354
  128. package/.output/server/_ssr/CodeField-B7oOmmwH.mjs +0 -143
  129. package/.output/server/_ssr/DashboardView-BF7PciP7.mjs +0 -2736
  130. package/.output/server/_ssr/LoopPlaybook-DC7QVvPY.mjs +0 -2105
  131. package/.output/server/_ssr/LoopView-DwSrm9Ja.mjs +0 -1300
  132. package/.output/server/_ssr/Modal-MaCG52Pv.mjs +0 -49
  133. package/.output/server/_ssr/artifactView-iuPsp5aa.mjs +0 -304
  134. package/.output/server/_ssr/artifacts-jQ-bfdhI.mjs +0 -2
  135. package/.output/server/_ssr/boot-CsEFT53K.mjs +0 -2
  136. package/.output/server/_ssr/db-CXownd1H.mjs +0 -2
  137. package/.output/server/_ssr/invite._token-CWl6kipD.mjs +0 -28
  138. package/.output/server/_ssr/logger-Gy-47z3k.mjs +0 -2
  139. package/.output/server/_ssr/loopApi-8_KgBapr.mjs +0 -109
  140. package/.output/server/_ssr/loopApi-ABi9SgWS.mjs +0 -564
  141. package/.output/server/_ssr/loops._loopId-SON_t43g.mjs +0 -30
  142. package/.output/server/_ssr/loops._loopId-p1KWsvMM.mjs +0 -1477
  143. package/.output/server/_ssr/notifyFns-BUCePuF3.mjs +0 -118
  144. package/.output/server/_ssr/router-Czxvra6l.mjs +0 -685
  145. package/.output/server/_ssr/runDiff--qLtU4FI.mjs +0 -2
  146. package/.output/server/_ssr/store-40eVzW1S.mjs +0 -2
  147. package/.output/server/_ssr/tokens-CbfoRC_s.mjs +0 -2
  148. package/.output/server/drizzle/0001_naive_silver_fox.sql +0 -25
  149. package/.output/server/drizzle/0002_funny_sage.sql +0 -12
  150. package/.output/server/drizzle/0003_optimal_slapstick.sql +0 -102
  151. package/.output/server/drizzle/0004_remove_grok_agent.sql +0 -5
  152. package/.output/server/drizzle/0005_heavy_gauntlet.sql +0 -8
  153. package/.output/server/drizzle/0006_tranquil_sharon_carter.sql +0 -1
  154. package/.output/server/drizzle/0007_faulty_starbolt.sql +0 -30
  155. package/.output/server/drizzle/0008_durable_lifecycle_evidence.sql +0 -15
  156. package/.output/server/drizzle/0009_ordinary_electro.sql +0 -1
  157. package/.output/server/drizzle/0010_workable_rage.sql +0 -14
  158. package/.output/server/drizzle/0011_steep_marvel_zombies.sql +0 -3
  159. package/.output/server/drizzle/0012_blocked_run_status.sql +0 -12
  160. package/.output/server/drizzle/0013_drop_run_outcome.sql +0 -2
  161. package/.output/server/drizzle/0014_luxuriant_scrambler.sql +0 -5
  162. package/.output/server/drizzle/0015_run_history_foundation.sql +0 -43
  163. package/.output/server/drizzle/0016_plural_machine_runs.sql +0 -4
  164. package/.output/server/drizzle/meta/0000_snapshot.json +0 -1333
  165. package/.output/server/drizzle/meta/0001_snapshot.json +0 -1498
  166. package/.output/server/drizzle/meta/0002_snapshot.json +0 -1576
  167. package/.output/server/drizzle/meta/0003_snapshot.json +0 -1666
  168. package/.output/server/drizzle/meta/0005_snapshot.json +0 -1666
  169. package/.output/server/drizzle/meta/0006_snapshot.json +0 -1672
  170. package/.output/server/drizzle/meta/0007_snapshot.json +0 -1787
  171. package/.output/server/drizzle/meta/0009_snapshot.json +0 -1793
  172. package/.output/server/drizzle/meta/0010_snapshot.json +0 -1891
  173. package/.output/server/drizzle/meta/0011_snapshot.json +0 -1872
  174. package/drizzle/0001_naive_silver_fox.sql +0 -25
  175. package/drizzle/0002_funny_sage.sql +0 -12
  176. package/drizzle/0003_optimal_slapstick.sql +0 -102
  177. package/drizzle/0004_remove_grok_agent.sql +0 -5
  178. package/drizzle/0005_heavy_gauntlet.sql +0 -8
  179. package/drizzle/0006_tranquil_sharon_carter.sql +0 -1
  180. package/drizzle/0007_faulty_starbolt.sql +0 -30
  181. package/drizzle/0008_durable_lifecycle_evidence.sql +0 -15
  182. package/drizzle/0009_ordinary_electro.sql +0 -1
  183. package/drizzle/0010_workable_rage.sql +0 -14
  184. package/drizzle/0011_steep_marvel_zombies.sql +0 -3
  185. package/drizzle/0012_blocked_run_status.sql +0 -12
  186. package/drizzle/0013_drop_run_outcome.sql +0 -2
  187. package/drizzle/0014_luxuriant_scrambler.sql +0 -5
  188. package/drizzle/0015_run_history_foundation.sql +0 -43
  189. package/drizzle/0016_plural_machine_runs.sql +0 -4
  190. package/drizzle/meta/0000_snapshot.json +0 -1333
  191. package/drizzle/meta/0001_snapshot.json +0 -1498
  192. package/drizzle/meta/0002_snapshot.json +0 -1576
  193. package/drizzle/meta/0003_snapshot.json +0 -1666
  194. package/drizzle/meta/0005_snapshot.json +0 -1666
  195. package/drizzle/meta/0006_snapshot.json +0 -1672
  196. package/drizzle/meta/0007_snapshot.json +0 -1787
  197. package/drizzle/meta/0009_snapshot.json +0 -1793
  198. package/drizzle/meta/0010_snapshot.json +0 -1891
  199. package/drizzle/meta/0011_snapshot.json +0 -1872
@@ -1,1300 +0,0 @@
1
- import { o as __toESM } from "../_runtime.mjs";
2
- import { g as require_jsx_runtime, h as require_react } from "../_libs/@base-ui/react+[...].mjs";
3
- import { Bt as parseChartSpec, _t as isTaskPath, c as CHART_ATTRS } from "./boot-3BNKujyp.mjs";
4
- import { A as tsShort, v as fmt, w as md, y as fnum } from "./ui-Ww2tTM2u.mjs";
5
- import { l as getChartRuns, s as getArtifacts } from "./loopApi-8_KgBapr.mjs";
6
- import { t as artifactKind } from "./artifactKind-BW0isV8L.mjs";
7
- import { t as purify } from "../_libs/dompurify.mjs";
8
- import { i as isMarkdown, r as ViewerHead, t as ArtifactBody } from "./artifactView-iuPsp5aa.mjs";
9
- import { n as ModalHead, t as Modal } from "./Modal-MaCG52Pv.mjs";
10
- import { n as import_lib, t as esm_default } from "../_libs/html-react-parser+[...].mjs";
11
- import { a as Scatter, c as CartesianGrid, d as ResponsiveContainer, i as XAxis, l as Tooltip, n as ScatterChart, o as Area, r as YAxis, s as Line, t as ComposedChart, u as Legend } from "../_libs/recharts+victory-vendor.mjs";
12
- //#region node_modules/.nitro/vite/services/ssr/assets/LoopView-DwSrm9Ja.js
13
- var import_react = /* @__PURE__ */ __toESM(require_react());
14
- var import_jsx_runtime = require_jsx_runtime();
15
- function buildBindingContext(runs) {
16
- const latest = {};
17
- const seen = /* @__PURE__ */ new Set();
18
- for (const r of runs) if (r.metrics && typeof r.metrics === "object" && !Array.isArray(r.metrics)) for (const k in r.metrics) {
19
- if (seen.has(k)) continue;
20
- seen.add(k);
21
- const v = r.metrics[k];
22
- if (typeof v === "number") latest[k] = fnum(v);
23
- }
24
- return { latest };
25
- }
26
- var TOKEN = /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g;
27
- var escapeHtml = (s) => s.replace(/[&<>"]/g, (c) => ({
28
- "&": "&amp;",
29
- "<": "&lt;",
30
- ">": "&gt;",
31
- "\"": "&quot;"
32
- })[c] ?? c);
33
- /** Interpolate `{{...}}` tokens. Values are HTML-escaped (they are agent-controlled data). */
34
- function resolveBindings(html, ctx) {
35
- return html.replace(TOKEN, (_m, path) => {
36
- const v = lookup(ctx, path);
37
- return v == null ? "—" : escapeHtml(v);
38
- });
39
- }
40
- function lookup(ctx, path) {
41
- const parts = path.split(".");
42
- if (parts[0] === "latest" && parts[1]) return ctx.latest[parts[1]];
43
- }
44
- var finiteMetric = (run, key) => {
45
- const value = run.metrics?.[key];
46
- return typeof value === "number" && Number.isFinite(value) ? value : null;
47
- };
48
- var chronological = (runs) => runs.slice().sort((a, b) => a.runIndex - b.runIndex);
49
- function seriesChartRows(runs, keys, x) {
50
- return chronological(runs).flatMap((run) => {
51
- const values = keys.map((key) => [key, finiteMetric(run, key)]);
52
- if (!values.some(([, value]) => value != null)) return [];
53
- const row = {
54
- __x: x === "time" ? run.ts : run.runIndex,
55
- __runIndex: run.runIndex,
56
- __status: run.status
57
- };
58
- for (const [key, value] of values) if (value != null) row[key] = value;
59
- return [row];
60
- });
61
- }
62
- function scatterChartRows(runs, xKey, yKey) {
63
- return chronological(runs).flatMap((run) => {
64
- const x = finiteMetric(run, xKey);
65
- const y = finiteMetric(run, yKey);
66
- return x == null || y == null ? [] : [{
67
- x,
68
- y,
69
- runIndex: run.runIndex,
70
- status: run.status,
71
- ts: run.ts
72
- }];
73
- });
74
- }
75
- function progressChartRows(runs, yKey) {
76
- return chronological(runs).flatMap((run) => {
77
- const y = finiteMetric(run, yKey);
78
- return y == null ? [] : [{
79
- x: run.runIndex,
80
- y,
81
- runIndex: run.runIndex,
82
- status: run.status,
83
- ts: run.ts
84
- }];
85
- });
86
- }
87
- /** Running best follows persisted kept decisions only. There is no renderer-side keep threshold. */
88
- function runningBest(rows, direction) {
89
- let best = null;
90
- const out = [];
91
- for (const row of rows) {
92
- if (row.status === "kept") best = best == null ? row.y : direction === "min" ? Math.min(best, row.y) : Math.max(best, row.y);
93
- if (best != null) out.push({
94
- x: row.x,
95
- y: best
96
- });
97
- }
98
- return out;
99
- }
100
- function chartDomain(values, requested) {
101
- if (Array.isArray(requested)) return requested;
102
- if (!values.length) return ["auto", "auto"];
103
- const min = Math.min(...values);
104
- const max = Math.max(...values);
105
- if (requested === "zero") return [Math.min(0, min), Math.max(0, max)];
106
- const span = max - min;
107
- const pad = span > 0 ? span * .1 : Math.max(Math.abs(max) * .05, 1);
108
- return [min - pad, max + pad];
109
- }
110
- var STROKES = [
111
- "var(--color-chart-1)",
112
- "var(--color-chart-2)",
113
- "var(--color-chart-3)",
114
- "var(--color-chart-4)",
115
- "var(--color-chart-5)"
116
- ];
117
- var HEIGHT = 190;
118
- var INITIAL = {
119
- width: 640,
120
- height: HEIGHT
121
- };
122
- var TICK = {
123
- fontSize: 10,
124
- fontFamily: "var(--font-mono)",
125
- fill: "var(--color-secondary)"
126
- };
127
- var TOOLTIP_STYLE = {
128
- background: "var(--color-surface)",
129
- border: "1px solid var(--color-wire)",
130
- borderRadius: 8,
131
- boxShadow: "none",
132
- padding: "6px 10px",
133
- fontFamily: "var(--font-mono)",
134
- fontSize: 11
135
- };
136
- var withUnit = (value, unit = "") => unit === "$" ? `$${fnum(value)}` : `${fnum(value)}${unit}`;
137
- var metricValues = (runs, keys) => runs.flatMap((run) => keys.map((key) => finiteMetric(run, key)).filter((value) => value != null));
138
- var axisDomain = (runs, keys, spec) => chartDomain(metricValues(runs, keys), spec.yDomain);
139
- var axisValue = (value, domain) => {
140
- if (typeof domain[0] !== "number" || typeof domain[1] !== "number") return fnum(value);
141
- const span = Math.abs(domain[1] - domain[0]);
142
- const digits = span < .001 ? 6 : span < .01 ? 5 : span < .1 ? 4 : span < 1 ? 3 : span < 10 ? 2 : span < 100 ? 1 : 0;
143
- return value.toFixed(digits);
144
- };
145
- function ChartFrame({ children, caption }) {
146
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figure", {
147
- className: "my-2 min-w-0",
148
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
149
- className: "min-w-0",
150
- style: { height: HEIGHT },
151
- children
152
- }), caption ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("figcaption", {
153
- className: "mt-1 flex flex-wrap gap-x-4 gap-y-1 text-label text-[var(--color-secondary)]",
154
- children: caption
155
- }) : null]
156
- });
157
- }
158
- function LoopChart({ runs, spec }) {
159
- if (spec.type === "scatter") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScatterPlot, {
160
- runs,
161
- spec
162
- });
163
- if (spec.type === "progress") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProgressPlot, {
164
- runs,
165
- spec
166
- });
167
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SeriesPlot, {
168
- runs,
169
- spec
170
- });
171
- }
172
- function SeriesPlot({ runs, spec }) {
173
- const gradientId = (0, import_react.useId)();
174
- const keys = spec.series.map((field) => field.key);
175
- const rows = seriesChartRows(runs, keys, spec.x);
176
- if (!rows.length) return null;
177
- const units = new Map(spec.series.map((field) => [field.key, field.unit ?? ""]));
178
- const domain = axisDomain(runs, keys, spec);
179
- const latest = /* @__PURE__ */ new Map();
180
- for (const row of rows) for (const key of keys) if (typeof row[key] === "number") latest.set(key, row[key]);
181
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChartFrame, {
182
- caption: spec.series.map((field, index) => {
183
- const value = latest.get(field.key);
184
- return value == null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
185
- className: "inline-flex items-center gap-1.5",
186
- children: [
187
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
188
- className: "inline-block h-[2px] w-3",
189
- style: { background: STROKES[index % STROKES.length] }
190
- }),
191
- field.label ?? field.key,
192
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
193
- className: "text-[var(--color-display)]",
194
- children: withUnit(value, field.unit)
195
- })
196
- ]
197
- }, field.key);
198
- }),
199
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResponsiveContainer, {
200
- width: "100%",
201
- height: "100%",
202
- initialDimension: INITIAL,
203
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ComposedChart, {
204
- data: rows,
205
- margin: {
206
- top: 6,
207
- right: 12,
208
- left: 0,
209
- bottom: 0
210
- },
211
- children: [
212
- spec.type === "area" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", {
213
- id: gradientId,
214
- x1: "0",
215
- y1: "0",
216
- x2: "0",
217
- y2: "1",
218
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
219
- offset: "5%",
220
- stopColor: STROKES[0],
221
- stopOpacity: .28
222
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
223
- offset: "95%",
224
- stopColor: STROKES[0],
225
- stopOpacity: .02
226
- })]
227
- }) }) : null,
228
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CartesianGrid, {
229
- vertical: false,
230
- stroke: "var(--color-hairline)"
231
- }),
232
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XAxis, {
233
- dataKey: "__x",
234
- type: spec.x === "runIndex" ? "number" : "category",
235
- domain: spec.x === "runIndex" ? ["dataMin", "dataMax"] : void 0,
236
- tick: TICK,
237
- tickLine: false,
238
- axisLine: false,
239
- tickMargin: 8,
240
- minTickGap: 28,
241
- tickFormatter: (value) => spec.x === "time" ? md(String(value)) : String(value)
242
- }),
243
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(YAxis, {
244
- width: 60,
245
- domain,
246
- allowDataOverflow: Array.isArray(spec.yDomain),
247
- tick: TICK,
248
- tickLine: false,
249
- axisLine: false,
250
- tickFormatter: (value) => axisValue(value, domain)
251
- }),
252
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip, {
253
- isAnimationActive: false,
254
- contentStyle: TOOLTIP_STYLE,
255
- labelFormatter: (value) => spec.x === "time" ? tsShort(String(value)) : `run #${value}`,
256
- formatter: (value, name, item) => [withUnit(Number(value), units.get(String(item?.dataKey))), String(name)]
257
- }),
258
- spec.type === "area" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Area, {
259
- ...seriesProps(spec.series[0], 0),
260
- connectNulls: true,
261
- dot: rows.length === 1 ? {
262
- r: 3.5,
263
- strokeWidth: 0
264
- } : false,
265
- fill: `url(#${gradientId})`
266
- }) : spec.series.map((field, index) => /* @__PURE__ */ (0, import_react.createElement)(Line, {
267
- ...seriesProps(field, index),
268
- key: field.key,
269
- connectNulls: true,
270
- dot: spec.colorBy === "status" ? statusDot(field.key) : rows.length === 1 ? {
271
- r: 3.5,
272
- strokeWidth: 0
273
- } : false
274
- }))
275
- ]
276
- })
277
- })
278
- });
279
- }
280
- function ScatterPlot({ runs, spec }) {
281
- const rows = scatterChartRows(runs, spec.xKey, spec.yKey);
282
- if (!rows.length) return null;
283
- const domain = chartDomain(rows.map((row) => row.y), spec.yDomain);
284
- const xDomain = chartDomain(rows.map((row) => row.x), "auto");
285
- const kept = rows.filter((row) => row.status === "kept");
286
- const other = rows.filter((row) => row.status !== "kept");
287
- const groups = spec.colorBy === "status" ? [{
288
- name: "Other",
289
- rows: other,
290
- fill: "var(--color-disabled)"
291
- }, {
292
- name: "Kept",
293
- rows: kept,
294
- fill: "var(--color-success)"
295
- }] : [{
296
- name: spec.yLabel ?? spec.yKey,
297
- rows,
298
- fill: STROKES[0]
299
- }];
300
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChartFrame, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResponsiveContainer, {
301
- width: "100%",
302
- height: "100%",
303
- initialDimension: INITIAL,
304
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ScatterChart, {
305
- margin: {
306
- top: 8,
307
- right: 12,
308
- left: 0,
309
- bottom: 8
310
- },
311
- children: [
312
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CartesianGrid, { stroke: "var(--color-hairline)" }),
313
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XAxis, {
314
- type: "number",
315
- dataKey: "x",
316
- name: spec.xLabel ?? spec.xKey,
317
- domain: xDomain,
318
- tickFormatter: (value) => axisValue(value, xDomain),
319
- tick: TICK,
320
- tickLine: false,
321
- axisLine: false
322
- }),
323
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(YAxis, {
324
- type: "number",
325
- dataKey: "y",
326
- name: spec.yLabel ?? spec.yKey,
327
- domain,
328
- allowDataOverflow: Array.isArray(spec.yDomain),
329
- width: 60,
330
- tickFormatter: (value) => axisValue(value, domain),
331
- tick: TICK,
332
- tickLine: false,
333
- axisLine: false
334
- }),
335
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip, {
336
- isAnimationActive: false,
337
- contentStyle: TOOLTIP_STYLE,
338
- cursor: {
339
- stroke: "var(--color-wire)",
340
- strokeDasharray: "3 3"
341
- },
342
- labelFormatter: (_, payload) => payload?.[0]?.payload ? `run #${payload[0].payload.runIndex}` : "",
343
- formatter: (value, name, item) => [withUnit(Number(value), String(item?.dataKey) === "x" ? spec.xUnit : spec.yUnit), String(name)]
344
- }),
345
- groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Scatter, {
346
- name: group.name,
347
- data: group.rows,
348
- fill: group.fill,
349
- isAnimationActive: false
350
- }, group.name)),
351
- spec.colorBy ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Legend, {
352
- verticalAlign: "top",
353
- align: "right"
354
- }) : null
355
- ]
356
- })
357
- }) });
358
- }
359
- function ProgressPlot({ runs, spec }) {
360
- const rows = progressChartRows(runs, spec.yKey);
361
- if (!rows.length) return null;
362
- const kept = rows.filter((row) => row.status === "kept");
363
- const other = rows.filter((row) => row.status !== "kept");
364
- const best = runningBest(rows, spec.direction);
365
- const domain = chartDomain(rows.map((row) => row.y), spec.yDomain);
366
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChartFrame, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResponsiveContainer, {
367
- width: "100%",
368
- height: "100%",
369
- initialDimension: INITIAL,
370
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ComposedChart, {
371
- margin: {
372
- top: 8,
373
- right: 12,
374
- left: 0,
375
- bottom: 8
376
- },
377
- children: [
378
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CartesianGrid, {
379
- vertical: false,
380
- stroke: "var(--color-hairline)"
381
- }),
382
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XAxis, {
383
- type: "number",
384
- dataKey: "x",
385
- domain: ["dataMin", "dataMax"],
386
- tick: TICK,
387
- tickLine: false,
388
- axisLine: false
389
- }),
390
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(YAxis, {
391
- type: "number",
392
- dataKey: "y",
393
- domain,
394
- allowDataOverflow: Array.isArray(spec.yDomain),
395
- width: 60,
396
- tickFormatter: (value) => axisValue(value, domain),
397
- tick: TICK,
398
- tickLine: false,
399
- axisLine: false
400
- }),
401
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip, {
402
- isAnimationActive: false,
403
- contentStyle: TOOLTIP_STYLE,
404
- labelFormatter: (_, payload) => payload?.[0]?.payload ? `run #${payload[0].payload.runIndex ?? payload[0].payload.x}` : ""
405
- }),
406
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Scatter, {
407
- name: "Other",
408
- data: other,
409
- fill: "var(--color-disabled)",
410
- isAnimationActive: false
411
- }),
412
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Scatter, {
413
- name: "Kept",
414
- data: kept,
415
- fill: "var(--color-success)",
416
- isAnimationActive: false
417
- }),
418
- best.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Line, {
419
- name: "Running best",
420
- data: best,
421
- dataKey: "y",
422
- type: "stepAfter",
423
- stroke: "var(--color-success)",
424
- strokeWidth: 2,
425
- dot: false,
426
- isAnimationActive: false
427
- }) : null,
428
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Legend, {
429
- verticalAlign: "top",
430
- align: "right"
431
- })
432
- ]
433
- })
434
- }) });
435
- }
436
- var seriesProps = (field, index) => ({
437
- dataKey: field.key,
438
- name: field.label ?? field.key,
439
- type: "linear",
440
- stroke: STROKES[index % STROKES.length],
441
- strokeWidth: 1.5,
442
- activeDot: {
443
- r: 3,
444
- strokeWidth: 0
445
- },
446
- isAnimationActive: false
447
- });
448
- var statusDot = (key) => (props) => {
449
- if (typeof props?.payload?.[key] !== "number") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", {});
450
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
451
- cx: props.cx,
452
- cy: props.cy,
453
- r: 2.8,
454
- fill: props.payload.__status === "kept" ? "var(--color-success)" : "var(--color-disabled)"
455
- });
456
- };
457
- var basename$2 = (path) => path.split("/").pop() || path;
458
- var pad = (n) => String(n).padStart(2, "0");
459
- /** A real calendar date within a sane range (no 2026-13-40, no year 10521). */
460
- function validDate(y, m, d) {
461
- if (y < 1970 || y > 2100 || m < 1 || m > 12 || d < 1 || d > 31) return false;
462
- const dt = new Date(Date.UTC(y, m - 1, d));
463
- return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
464
- }
465
- var DATE_RE = /(\d{4})(?:([-_])(\d{2})\2(\d{2})|(\d{2})(\d{2}))/g;
466
- /** First valid date found in the file's basename, else null. */
467
- function parseFilenameDate(path) {
468
- const name = basename$2(path);
469
- DATE_RE.lastIndex = 0;
470
- for (let m = DATE_RE.exec(name); m; m = DATE_RE.exec(name)) {
471
- const before = name[m.index - 1];
472
- const after = name[m.index + m[0].length];
473
- if (before && /\d/.test(before) || after && /\d/.test(after)) continue;
474
- const y = Number(m[1]);
475
- const mo = Number(m[3] ?? m[5]);
476
- const d = Number(m[4] ?? m[6]);
477
- if (validDate(y, mo, d)) return `${m[1]}-${pad(mo)}-${pad(d)}`;
478
- }
479
- return null;
480
- }
481
- /** Local calendar day of an ISO timestamp (the calendar renders user-local). */
482
- function localDay(iso) {
483
- const d = new Date(iso);
484
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
485
- }
486
- /**
487
- * A calendar day from a front-matter `date:` value (stored RAW — validity is
488
- * decided here). Forgiving about the shape: takes the leading `YYYY-MM-DD` (or
489
- * `/`/`_` separators, or a full ISO timestamp's date part) and validates it's a
490
- * real day. Returns null for anything unparseable/invalid (fall through to
491
- * filename/sync).
492
- */
493
- function parseMetaDate(raw) {
494
- if (typeof raw !== "string") return null;
495
- const m = /^\s*(\d{4})[-/_](\d{2})[-/_](\d{2})/.exec(raw);
496
- if (!m) return null;
497
- const y = Number(m[1]);
498
- const mo = Number(m[2]);
499
- const d = Number(m[3]);
500
- return validDate(y, mo, d) ? `${m[1]}-${pad(mo)}-${pad(d)}` : null;
501
- }
502
- /** Date one artifact: front-matter `date:` first (authoritative), filename next,
503
- * sync time as the marked fallback. */
504
- function productDate(file) {
505
- const fromMeta = parseMetaDate(file.meta?.date);
506
- if (fromMeta) return {
507
- date: fromMeta,
508
- source: "frontmatter"
509
- };
510
- const fromName = parseFilenameDate(file.path);
511
- return fromName ? {
512
- date: fromName,
513
- source: "filename"
514
- } : {
515
- date: localDay(file.updatedAt),
516
- source: "sync"
517
- };
518
- }
519
- /**
520
- * `match` glob → RegExp. `*` matches within a path segment (never `/`); all
521
- * other characters are literal. A pattern with no `/` matches against the
522
- * BASENAME (so `*.md` means "any markdown file, anywhere"), a pattern with
523
- * one matches the whole loop-relative path.
524
- */
525
- function globToRegExp(pattern) {
526
- const rx = pattern.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
527
- return new RegExp(`^${rx}$`);
528
- }
529
- /** The artifacts a `match` pattern selects (no pattern ⇒ all). */
530
- function matchArtifacts(files, pattern) {
531
- if (!pattern) return files;
532
- const rx = globToRegExp(pattern);
533
- const onBase = !pattern.includes("/");
534
- return files.filter((f) => rx.test(onBase ? basename$2(f.path) : f.path));
535
- }
536
- /**
537
- * The newest artifact a pattern selects - by product date (filename first)
538
- * descending, sync time as the tiebreak. This keeps "the newest digest"
539
- * correct even when an OLD file happens to re-sync last.
540
- */
541
- function newestMatch(files, pattern) {
542
- const matched = matchArtifacts(files, pattern);
543
- let best;
544
- let bestKey = "";
545
- for (const f of matched) {
546
- const key = `${productDate(f).date}|${f.updatedAt}`;
547
- if (!best || key > bestKey) {
548
- best = f;
549
- bestKey = key;
550
- }
551
- }
552
- return best;
553
- }
554
- /**
555
- * `<loop-embed file="reports/digest-2026-07-01.md">` /
556
- * `<loop-embed match="reports/digest-*.md">` - a loop's own produced file
557
- * embedded in its dashboard. `file` targets one exact path; `match` resolves to
558
- * the NEWEST matching artifact (filename date first, sync time as tiebreak), so
559
- * "embed the latest digest" keeps working without the agent editing the
560
- * template every run. `match` never selects the loop's task file - the spec is
561
- * not a product (same rule as the calendar's default set), and its frequent
562
- * edit-syncs would pin it as "newest" under a broad glob like `*.md`; an exact
563
- * `file=` path may still target it. Content renders through the shared
564
- * artifact viewer (markdown via .taskmd, mono pre for other text, download
565
- * notice for binary), collapsed at 300px with a fade + "show all" toggle; the
566
- * `full` attribute opts out of the collapse.
567
- */
568
- /** Collapsed preview height. A pixel cap, not a line clamp - markdown blocks
569
- * (tables, code) have uneven line heights, so pixels are the only stable cut. */
570
- var COLLAPSE_PX = 300;
571
- var norm = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
572
- function LoopEmbed({ loopId, artifacts, file, match, full, taskFile }) {
573
- const requested = file ?? match;
574
- const shell = "min-w-0 overflow-hidden rounded-card border border-hairline bg-surface shadow-card";
575
- if (!requested) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
576
- className: shell,
577
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
578
- className: "px-5 py-5 font-mono text-label text-disabled",
579
- children: "<loop-embed> needs file=\"<path>\" or match=\"<glob>\""
580
- })
581
- });
582
- if (artifacts == null) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
583
- className: shell,
584
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewerHead, { path: requested }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
585
- className: "px-5 py-6 text-label text-secondary",
586
- children: "Loading…"
587
- })]
588
- });
589
- const matchable = file ? artifacts : artifacts.filter((a) => !isTaskPath(taskFile, a.path));
590
- const target = file ? artifacts.find((a) => norm(a.path) === norm(file)) : newestMatch(matchable, match);
591
- if (!target) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
592
- className: shell,
593
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewerHead, { path: requested }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
594
- className: "px-5 py-6 text-body text-disabled",
595
- children: "No synced file matches yet. It appears here after the loop's next run syncs it."
596
- })]
597
- });
598
- const meta = `${match ? `newest of ${matchArtifacts(matchable, match).length} matching ${match} · ` : ""}synced ${fmt(target.updatedAt)}`;
599
- const kind = artifactKind(target.path);
600
- const collapse = !full && !(kind === "html" || kind === "image") && !target.binary && !target.oversize;
601
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
602
- className: shell,
603
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewerHead, {
604
- path: target.path,
605
- meta,
606
- action: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
607
- href: "#files",
608
- className: "text-caption font-medium text-interactive transition-colors hover:text-display",
609
- children: "Open in files →"
610
- })
611
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleBody, {
612
- collapse,
613
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArtifactBody, {
614
- loopId,
615
- file: target
616
- })
617
- })]
618
- });
619
- }
620
- /**
621
- * The collapse wrapper: clipping happens on THIS wrapper (overflow-hidden +
622
- * max-height), never on the text nodes, with a fade overlay above the cut and
623
- * the toggle in its own footer. The toggle appears only when the content
624
- * actually overflows the cap (measured post-render, re-checked as the lazy
625
- * content lands).
626
- */
627
- function CollapsibleBody({ collapse, children }) {
628
- const ref = (0, import_react.useRef)(null);
629
- const [expanded, setExpanded] = (0, import_react.useState)(false);
630
- const [overflowing, setOverflowing] = (0, import_react.useState)(false);
631
- (0, import_react.useLayoutEffect)(() => {
632
- const el = ref.current;
633
- if (!el) return;
634
- const over = el.scrollHeight > 301;
635
- if (over !== overflowing) setOverflowing(over);
636
- });
637
- const collapsed = collapse && !expanded && overflowing;
638
- if (!collapse) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
639
- className: "min-w-0",
640
- children
641
- });
642
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
643
- className: "relative min-w-0",
644
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
645
- ref,
646
- className: collapsed ? "overflow-hidden" : "",
647
- style: collapsed ? { maxHeight: COLLAPSE_PX } : void 0,
648
- children
649
- }), collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
650
- "aria-hidden": true,
651
- className: "pointer-events-none absolute inset-x-0 bottom-0 h-16",
652
- style: { background: "linear-gradient(to bottom, transparent, var(--color-surface))" }
653
- })]
654
- }), overflowing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
655
- className: "flex border-t border-hairline px-3.5 py-1.5",
656
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
657
- type: "button",
658
- onClick: () => setExpanded(!expanded),
659
- className: "cursor-pointer border-none bg-transparent p-0 text-caption font-medium text-secondary transition-colors hover:text-display",
660
- children: expanded ? "Collapse ▴" : "Show all ▾"
661
- })
662
- })] });
663
- }
664
- /**
665
- * `<loop-calendar match="reports/*.md">` - the loop's products on a month
666
- * grid. Each product lands on the day parsed from its FILENAME (ISO-ish
667
- * patterns), falling back to its sync time (marked with a dashed chip - sync
668
- * time can misattribute a re-synced old file, so the fallback is visible, not
669
- * silent). Clicking a product reviews its full body in the shared Modal
670
- * (focus trap, Esc, scroll lock come from Base UI Dialog - the kanban card
671
- * pattern); close returns to the grid. `match` is optional - the default is
672
- * every synced artifact except the task file.
673
- *
674
- * Monday-start (ISO), mono day numbers top-right, today inverted. Chips carry
675
- * the product's basename (max 2 per day + a "+N" overflow); under ~620px of
676
- * CONTAINER width they collapse to plain dots (measured, not a viewport media
677
- * query - the dashboard box can be narrow on a wide screen).
678
- */
679
- var DOW = [
680
- "Mon",
681
- "Tue",
682
- "Wed",
683
- "Thu",
684
- "Fri",
685
- "Sat",
686
- "Sun"
687
- ];
688
- var DOT_MODE_BELOW = 620;
689
- var CHIPS_PER_DAY = 2;
690
- var monthOf = (day) => day.slice(0, 7);
691
- var basename$1 = (path) => path.split("/").pop() || path;
692
- /** Human label for how a product got its day (front matter is authoritative). */
693
- function dateSourceLabel(source) {
694
- if (source === "frontmatter") return "dated by front matter";
695
- if (source === "filename") return "dated by filename";
696
- return "dated by sync time (no date in front matter/filename)";
697
- }
698
- var monthIndex = (ym) => {
699
- const [y, m] = ym.split("-").map(Number);
700
- return y * 12 + m;
701
- };
702
- /**
703
- * The shown month clamped back into the data: an exact hit stays; a month
704
- * whose products all vanished (poll refresh) snaps to the nearest remaining
705
- * one; no pick defaults to the newest month. `months` must be non-empty.
706
- */
707
- function clampMonth(months, shown) {
708
- if (shown && months.includes(shown)) return shown;
709
- let best = months[months.length - 1];
710
- if (shown) {
711
- const target = monthIndex(shown);
712
- for (const m of months) if (Math.abs(monthIndex(m) - target) < Math.abs(monthIndex(best) - target)) best = m;
713
- }
714
- return best;
715
- }
716
- var monthLabel = (ym) => {
717
- const [y, m] = ym.split("-").map(Number);
718
- return `${new Date(Date.UTC(y, m - 1, 1)).toLocaleString("en-US", {
719
- month: "long",
720
- timeZone: "UTC"
721
- })} ${y}`;
722
- };
723
- /** Chip text: basename without extension or a trailing date (digest-2026-07-01.md → "digest"). */
724
- function chipName(path) {
725
- const noExt = (path.split("/").pop() || path).replace(/\.[a-z0-9]+$/i, "");
726
- return noExt.replace(/[-_]?\d{4}([-_]?)\d{2}\1\d{2}$/, "") || noExt;
727
- }
728
- /** Measured width of a container element (0 until first measure). */
729
- function useContainerWidth(ref) {
730
- const [width, setWidth] = (0, import_react.useState)(0);
731
- (0, import_react.useEffect)(() => {
732
- const el = ref.current;
733
- if (!el || typeof ResizeObserver === "undefined") return;
734
- const ro = new ResizeObserver((entries) => {
735
- const w = entries[0]?.contentRect.width;
736
- if (w != null) setWidth(w);
737
- });
738
- ro.observe(el);
739
- return () => ro.disconnect();
740
- }, [ref]);
741
- return width;
742
- }
743
- function LoopCalendar({ loopId, artifacts, match, taskFile }) {
744
- const rootRef = (0, import_react.useRef)(null);
745
- const width = useContainerWidth(rootRef);
746
- const dotMode = width > 0 && width < DOT_MODE_BELOW;
747
- const products = (0, import_react.useMemo)(() => {
748
- const files = matchArtifacts(artifacts ?? [], match);
749
- return (match ? files : files.filter((f) => !isTaskPath(taskFile, f.path))).map((f) => ({
750
- file: f,
751
- ...productDate(f)
752
- }));
753
- }, [
754
- artifacts,
755
- match,
756
- taskFile
757
- ]);
758
- const byDay = (0, import_react.useMemo)(() => {
759
- const m = /* @__PURE__ */ new Map();
760
- for (const p of products) {
761
- const list = m.get(p.date) ?? [];
762
- list.push(p);
763
- m.set(p.date, list);
764
- }
765
- for (const list of m.values()) list.sort((a, b) => a.file.path < b.file.path ? -1 : 1);
766
- return m;
767
- }, [products]);
768
- const months = (0, import_react.useMemo)(() => [...new Set(products.map((p) => monthOf(p.date)))].sort(), [products]);
769
- const [shown, setShown] = (0, import_react.useState)(null);
770
- const [reviewing, setReviewing] = (0, import_react.useState)(null);
771
- const activeReview = reviewing && products.some((p) => p.file.path === reviewing.file.path) ? reviewing : null;
772
- if (artifacts == null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
773
- ref: rootRef,
774
- className: "min-w-0",
775
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
776
- className: "py-4 text-label text-secondary",
777
- children: "Loading…"
778
- })
779
- });
780
- if (!products.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
781
- ref: rootRef,
782
- className: "min-w-0",
783
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
784
- className: "rounded-card border border-hairline px-5 py-8 text-center text-body text-disabled",
785
- children: "No products yet. Files the loop writes appear here after its next run syncs them."
786
- })
787
- });
788
- const month = clampMonth(months, shown);
789
- const mi = months.indexOf(month);
790
- const today = localDay((/* @__PURE__ */ new Date()).toISOString());
791
- const [y, m] = month.split("-").map(Number);
792
- const lead = (new Date(y, m - 1, 1).getDay() + 6) % 7;
793
- const dayCount = new Date(y, m, 0).getDate();
794
- const trail = (7 - (lead + dayCount) % 7) % 7;
795
- const monthCount = products.filter((p) => monthOf(p.date) === month).length;
796
- const navBtn = "inline-flex size-7 cursor-pointer items-center justify-center rounded-control border border-hairline bg-surface text-body leading-none text-secondary transition-colors hover:border-wire hover:bg-raised hover:text-display disabled:cursor-default disabled:opacity-30 disabled:hover:border-hairline disabled:hover:bg-surface";
797
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
798
- ref: rootRef,
799
- className: "min-w-0",
800
- children: [
801
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
802
- className: "mb-3 flex flex-wrap items-center gap-x-3 gap-y-2",
803
- children: [
804
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
805
- className: "flex items-center gap-1.5",
806
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
807
- type: "button",
808
- className: navBtn,
809
- disabled: mi <= 0,
810
- onClick: () => setShown(months[mi - 1]),
811
- "aria-label": "previous month",
812
- children: "‹"
813
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
814
- type: "button",
815
- className: navBtn,
816
- disabled: mi >= months.length - 1,
817
- onClick: () => setShown(months[mi + 1]),
818
- "aria-label": "next month",
819
- children: "›"
820
- })]
821
- }),
822
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
823
- className: "text-body font-semibold tracking-tight text-display",
824
- children: monthLabel(month)
825
- }),
826
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
827
- className: "ml-auto rounded-full bg-raised px-2.5 py-0.5 text-caption font-medium text-secondary",
828
- children: [
829
- monthCount,
830
- " product",
831
- monthCount === 1 ? "" : "s"
832
- ]
833
- })
834
- ]
835
- }),
836
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
837
- className: "grid min-w-0 grid-cols-7 overflow-hidden rounded-card border-l border-t border-hairline",
838
- children: [
839
- DOW.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
840
- className: "border-b border-r border-hairline bg-raised/50 px-2 py-1.5 text-right text-micro font-semibold uppercase tracking-wide text-disabled",
841
- children: d
842
- }, d)),
843
- Array.from({ length: lead }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-r border-hairline bg-raised/20" }, `lead${i}`)),
844
- Array.from({ length: dayCount }, (_, i) => {
845
- const day = `${month}-${String(i + 1).padStart(2, "0")}`;
846
- const fs = byDay.get(day) ?? [];
847
- const isToday = day === today;
848
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
849
- className: `relative min-w-0 border-b border-r border-hairline px-1.5 pb-1.5 transition-colors ${dotMode ? "min-h-[48px] pt-5" : "min-h-[76px] pt-6"} ${isToday ? "bg-interactive-soft/40" : "bg-surface"}`,
850
- children: [
851
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
852
- className: `absolute right-1.5 top-1 inline-flex h-[15px] min-w-[15px] items-center justify-center font-mono text-micro ${isToday ? "rounded-full bg-display px-1 font-semibold text-paper" : fs.length ? "text-secondary" : "text-disabled"}`,
853
- children: i + 1
854
- }),
855
- fs.slice(0, CHIPS_PER_DAY).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProductChip, {
856
- product: p,
857
- dot: dotMode,
858
- onPick: () => setReviewing(p)
859
- }, p.file.path)),
860
- fs.length > CHIPS_PER_DAY && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
861
- type: "button",
862
- onClick: () => setReviewing(fs[CHIPS_PER_DAY]),
863
- className: "mt-1 block cursor-pointer border-none bg-transparent p-0 px-1 font-mono text-[9.5px] font-medium text-disabled transition-colors hover:text-display",
864
- children: [
865
- "+",
866
- fs.length - CHIPS_PER_DAY,
867
- " more"
868
- ]
869
- })
870
- ]
871
- }, day);
872
- }),
873
- Array.from({ length: trail }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-r border-hairline bg-raised/20" }, `trail${i}`))
874
- ]
875
- }),
876
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
877
- className: "mt-3 flex flex-wrap gap-x-4 gap-y-1.5",
878
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
879
- className: "inline-flex items-center gap-1.5 text-caption font-medium text-secondary",
880
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "inline-block h-2.5 w-3.5 rounded-[3px] border border-hairline bg-raised" }), " Dated by filename"]
881
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
882
- className: "inline-flex items-center gap-1.5 text-caption font-medium text-secondary",
883
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "inline-block h-2.5 w-3.5 rounded-[3px] border border-dashed border-wire bg-raised" }), " Fallback: dated by sync time"]
884
- })]
885
- }),
886
- activeReview && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
887
- open: true,
888
- onClose: () => setReviewing(null),
889
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
890
- title: activeReview.file.meta?.title?.trim() || basename$1(activeReview.file.path),
891
- sub: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
892
- className: "font-mono",
893
- children: [
894
- activeReview.file.path,
895
- " · ",
896
- dateSourceLabel(activeReview.source),
897
- " · synced",
898
- " ",
899
- fmt(activeReview.file.updatedAt)
900
- ]
901
- })
902
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
903
- className: "mt-4 min-w-0",
904
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArtifactBody, {
905
- loopId,
906
- file: activeReview.file
907
- })
908
- })]
909
- })
910
- ]
911
- });
912
- }
913
- /** One product on a day cell: a named chip, or a plain dot in narrow containers. */
914
- function ProductChip({ product, dot, onPick }) {
915
- const fallback = product.source === "sync";
916
- const title = product.file.path + (fallback ? " · dated by sync time" : "");
917
- if (dot) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
918
- type: "button",
919
- onClick: onPick,
920
- title,
921
- "aria-label": product.file.path,
922
- className: `mr-1 mt-1 inline-block size-2 cursor-pointer rounded-full border-none p-0 transition-transform hover:scale-125 ${fallback ? "bg-disabled" : "bg-interactive"}`
923
- });
924
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
925
- type: "button",
926
- onClick: onPick,
927
- title,
928
- className: `mt-1 block w-full min-w-0 cursor-pointer truncate rounded-full border border-hairline bg-raised px-2 py-0.5 text-left font-mono text-micro text-secondary transition-colors hover:border-wire hover:bg-surface hover:text-display ${fallback ? "border-dashed" : ""}`,
929
- children: chipName(product.file.path)
930
- });
931
- }
932
- /**
933
- * `<loop-kanban columns="research,in-progress,done" match="notes/*.md">` - a
934
- * COLLECTION VIEW over the loop's front-matter-typed markdown products, rendered
935
- * as a board. There is NO board file: each artifact IS a card, grouped into the
936
- * column whose name equals the artifact's front-matter `type`. A card "moves"
937
- * when a run edits that artifact's `type` in place (the per-run diff shows the
938
- * move).
939
- *
940
- * - `columns` (REQUIRED): comma-separated, explicit order = flow direction, one
941
- * board column each.
942
- * - Overflow: any typed artifact whose `type` matches no declared column lands
943
- * in an automatic trailing "Other" column, so a typo stays visible and a card
944
- * is never silently dropped. The overflow column is omitted when empty.
945
- * - `match` (optional glob, same convention as loop-embed/loop-calendar): scopes
946
- * which artifacts participate; default = every markdown product with a `type`.
947
- * Untyped artifacts never appear, and the task file is always excluded (its
948
- * status is the TASK chip, not a kanban type).
949
- * - Cards: minimal - title (fallback filename) + a small date when the product
950
- * is actually dated. Sorted date desc, then sync-time desc.
951
- * - Click a card to review its full body in the shared Modal (focus trap, Esc,
952
- * scroll lock come from Base UI Dialog); close returns to the board.
953
- *
954
- * Layout: the board row scrolls INSIDE its own pane (`min-w-0 overflow-x-auto`),
955
- * columns are `shrink-0` fixed-width - a wide board never widens the dashboard
956
- * box or forces a page-level horizontal scrollbar (the Timeline strip rule).
957
- */
958
- var OVERFLOW_LABEL = "Other";
959
- /** Bucket key for the automatic overflow column. A unique Symbol (not the display
960
- * string) so it can never collide with an author-declared column named 'Other'. */
961
- var OVERFLOW_KEY = Symbol("kanban-overflow");
962
- /** React key for the overflow row. Column names are comma-split, so a name can
963
- * never contain a comma - this sentinel cannot collide with a declared column. */
964
- var OVERFLOW_REACT_KEY = "overflow,";
965
- var basename = (path) => path.split("/").pop() || path;
966
- /** Sort key: product date desc, then sync time desc (newest work first). Both
967
- * parts are lexically ordered (YYYY-MM-DD / ISO), so a plain string compare on
968
- * the descending key ranks them. */
969
- var sortKey = (c) => `${c.date.date}|${c.file.updatedAt}`;
970
- function LoopKanban({ loopId, artifacts, columns, match, taskFile }) {
971
- const [reviewing, setReviewing] = (0, import_react.useState)(null);
972
- const cols = [...new Set((columns ?? "").split(",").map((s) => s.trim()).filter(Boolean))];
973
- const shell = "min-w-0";
974
- if (!cols.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
975
- className: `${shell} rounded-card border border-hairline bg-surface shadow-card`,
976
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
977
- className: "px-5 py-5 font-mono text-label text-disabled",
978
- children: "<loop-kanban> needs columns=\"<a,b,c>\""
979
- })
980
- });
981
- if (artifacts == null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
982
- className: shell,
983
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
984
- className: "py-4 text-label text-secondary",
985
- children: "Loading…"
986
- })
987
- });
988
- const cards = matchArtifacts(artifacts, match).filter((a) => isMarkdown(a.path) && !!a.meta?.type && !isTaskPath(taskFile, a.path)).map((file) => ({
989
- file,
990
- title: file.meta?.title?.trim() || basename(file.path),
991
- date: productDate(file)
992
- }));
993
- const declared = new Set(cols);
994
- const grouped = /* @__PURE__ */ new Map();
995
- for (const card of cards) {
996
- const type = card.file.meta.type.trim();
997
- const key = declared.has(type) ? type : OVERFLOW_KEY;
998
- const list = grouped.get(key) ?? [];
999
- list.push(card);
1000
- grouped.set(key, list);
1001
- }
1002
- for (const list of grouped.values()) list.sort((a, b) => sortKey(a) < sortKey(b) ? 1 : -1);
1003
- const board = cols.map((name) => ({
1004
- name,
1005
- cards: grouped.get(name) ?? [],
1006
- overflow: false
1007
- }));
1008
- const overflow = grouped.get(OVERFLOW_KEY) ?? [];
1009
- if (overflow.length) board.push({
1010
- name: OVERFLOW_LABEL,
1011
- cards: overflow,
1012
- overflow: true
1013
- });
1014
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1015
- className: `${shell} flex max-h-[420px] gap-3 overflow-x-auto pb-1`,
1016
- children: [board.map((col) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1017
- className: "flex min-h-0 w-[248px] shrink-0 flex-col",
1018
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1019
- className: "mb-2 flex shrink-0 items-center gap-2 border-b border-hairline pb-1.5",
1020
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1021
- className: `text-label font-semibold ${col.overflow ? "text-disabled" : "text-primary"}`,
1022
- children: col.name
1023
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1024
- className: "text-caption text-disabled",
1025
- children: col.cards.length
1026
- })]
1027
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColumnCards, {
1028
- count: col.cards.length,
1029
- children: col.cards.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1030
- className: "shrink-0 rounded-control border border-dashed border-hairline px-2.5 py-3 text-center text-caption text-disabled",
1031
- children: "Empty"
1032
- }) : col.cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KanbanCard, {
1033
- card,
1034
- onOpen: () => setReviewing(card)
1035
- }, card.file.path))
1036
- })]
1037
- }, col.overflow ? OVERFLOW_REACT_KEY : col.name)), reviewing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
1038
- open: true,
1039
- onClose: () => setReviewing(null),
1040
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
1041
- title: reviewing.title,
1042
- sub: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1043
- className: "font-mono",
1044
- children: [
1045
- reviewing.file.path,
1046
- reviewing.date.source !== "sync" && ` · ${reviewing.date.date}`,
1047
- " · synced",
1048
- " ",
1049
- fmt(reviewing.file.updatedAt)
1050
- ]
1051
- })
1052
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1053
- className: "mt-4 min-w-0",
1054
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArtifactBody, {
1055
- loopId,
1056
- file: reviewing.file
1057
- })
1058
- })]
1059
- })]
1060
- });
1061
- }
1062
- /**
1063
- * A column's scrollable card list. Cards are shrink-0 (they must overflow the
1064
- * height-capped board, never compress to fit it); when there IS overflow below
1065
- * the fold, a bottom fade + "scroll" pill signals it, disappearing at the end.
1066
- */
1067
- function ColumnCards({ count, children }) {
1068
- const ref = (0, import_react.useRef)(null);
1069
- const [below, setBelow] = (0, import_react.useState)(false);
1070
- const update = () => {
1071
- const el = ref.current;
1072
- if (el) setBelow(el.scrollTop + el.clientHeight < el.scrollHeight - 4);
1073
- };
1074
- (0, import_react.useEffect)(update, [count]);
1075
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1076
- className: "relative flex min-h-0 flex-col",
1077
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1078
- ref,
1079
- onScroll: update,
1080
- className: "flex min-h-0 flex-col gap-2 overflow-y-auto pb-1 pr-0.5",
1081
- children
1082
- }), below && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1083
- className: "pointer-events-none absolute inset-x-0 bottom-0 flex h-12 items-end justify-center bg-gradient-to-t from-surface to-transparent",
1084
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1085
- className: "mb-1 rounded-full border border-hairline bg-surface px-2 py-0.5 text-micro text-secondary shadow-card",
1086
- children: "↓ scroll"
1087
- })
1088
- })]
1089
- });
1090
- }
1091
- /** One artifact as a card: title + small date, click to review it in the modal. */
1092
- function KanbanCard({ card, onOpen }) {
1093
- const dated = card.date.source !== "sync";
1094
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1095
- className: "min-w-0 shrink-0 overflow-hidden rounded-control border border-hairline bg-surface shadow-card transition-colors hover:border-wire",
1096
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
1097
- type: "button",
1098
- onClick: onOpen,
1099
- className: "flex w-full min-w-0 cursor-pointer flex-col items-start gap-1 border-none bg-transparent px-2.5 py-2 text-left",
1100
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1101
- className: "w-full min-w-0 truncate text-meta text-primary",
1102
- title: card.file.path,
1103
- children: card.title
1104
- }), dated && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1105
- className: "font-mono text-micro text-disabled",
1106
- children: card.date.date
1107
- })]
1108
- })
1109
- });
1110
- }
1111
- /**
1112
- * Renders a loop's generative-UI template (agent-authored HTML on `Job.ui`).
1113
- *
1114
- * Pipeline: interpolate `{{ ... }}` scalar bindings with live run data → DOMPurify
1115
- * sanitize (allowlisted HTML subset; NO script/handlers/raw-svg) → parse to React,
1116
- * swapping the irreducible data primitives for their renderers. Everything
1117
- * else (A/B panels, stat tiles, layout, text) is the agent's own HTML — there are
1118
- * NO opinionated panel components.
1119
- *
1120
- * <loop-chart type="line" x="runIndex" series="mrr:MRR:$"></loop-chart>
1121
- * <loop-embed match="reports/digest-*.md"></loop-embed> newest matching artifact, embedded
1122
- * <loop-calendar match="reports/*.md"></loop-calendar> month calendar of produced files
1123
- * <loop-kanban columns="a,b,c" match="notes/*.md"> typed products as a board, columns = type
1124
- *
1125
- * Registering a new primitive means moving three things together: LOOP_TAGS +
1126
- * the sanitizer config below, the parser swap in `options`, and the skill's
1127
- * authoring docs (references/dashboard.md plus concise run-prompt examples) - the sanitizer
1128
- * allowlist and the skill prose must never drift apart.
1129
- */
1130
- var LOOP_TAGS = [
1131
- "loop-chart",
1132
- "loop-embed",
1133
- "loop-calendar",
1134
- "loop-kanban"
1135
- ];
1136
- /** Data-bearing attributes on the loop-* primitives (all parsed by us, never markup). */
1137
- var LOOP_ATTRS = [
1138
- ...CHART_ATTRS,
1139
- "file",
1140
- "match",
1141
- "full",
1142
- "columns"
1143
- ];
1144
- var ARTIFACT_RETRY_MAX = 3;
1145
- var ARTIFACT_RETRY_MS = 4e3;
1146
- var SANITIZE_CONFIG = {
1147
- ALLOWED_TAGS: [
1148
- "h1",
1149
- "h2",
1150
- "h3",
1151
- "h4",
1152
- "h5",
1153
- "p",
1154
- "b",
1155
- "strong",
1156
- "i",
1157
- "em",
1158
- "u",
1159
- "s",
1160
- "span",
1161
- "div",
1162
- "ul",
1163
- "ol",
1164
- "li",
1165
- "table",
1166
- "thead",
1167
- "tbody",
1168
- "tr",
1169
- "td",
1170
- "th",
1171
- "code",
1172
- "pre",
1173
- "br",
1174
- "hr",
1175
- "small",
1176
- "section",
1177
- "header",
1178
- "footer",
1179
- "a",
1180
- "figure",
1181
- "figcaption",
1182
- "mark",
1183
- ...LOOP_TAGS
1184
- ],
1185
- ALLOWED_ATTR: [
1186
- "style",
1187
- "class",
1188
- "href",
1189
- "title",
1190
- "target",
1191
- "rel",
1192
- ...LOOP_ATTRS
1193
- ],
1194
- ADD_TAGS: LOOP_TAGS,
1195
- CUSTOM_ELEMENT_HANDLING: {
1196
- tagNameCheck: new RegExp(`^(?:${LOOP_TAGS.join("|")})$`),
1197
- attributeNameCheck: new RegExp(`^(?:${LOOP_ATTRS.join("|")})$`),
1198
- allowCustomizedBuiltInElements: false
1199
- }
1200
- };
1201
- purify.addHook("uponSanitizeAttribute", (node, data) => {
1202
- const tag = node.nodeName?.toLowerCase();
1203
- if (tag && LOOP_TAGS.includes(tag) && LOOP_ATTRS.includes(data.attrName)) data.forceKeepAttr = true;
1204
- });
1205
- function LoopView({ html, runs, loopId, taskFile }) {
1206
- const clean = (0, import_react.useMemo)(() => {
1207
- const ctx = buildBindingContext(runs);
1208
- return purify.sanitize(resolveBindings(html, ctx), SANITIZE_CONFIG);
1209
- }, [html, runs]);
1210
- const wantsCharts = /<loop-chart\b/.test(clean);
1211
- const [chartData, setChartData] = (0, import_react.useState)(null);
1212
- const chartRuns = chartData?.loopId === loopId ? chartData.runs : null;
1213
- const wantsArtifacts = /<loop-(embed|calendar|kanban)\b/.test(clean);
1214
- const [artifacts, setArtifacts] = (0, import_react.useState)(null);
1215
- const newestRunId = runs[0]?.id;
1216
- const newestRunLive = runs[0]?.running === true;
1217
- (0, import_react.useEffect)(() => {
1218
- if (!wantsCharts) return;
1219
- let alive = true;
1220
- getChartRuns({ data: { loopId } }).then((list) => alive && setChartData({
1221
- loopId,
1222
- runs: list
1223
- })).catch(() => {});
1224
- return () => {
1225
- alive = false;
1226
- };
1227
- }, [
1228
- wantsCharts,
1229
- loopId,
1230
- newestRunId,
1231
- newestRunLive
1232
- ]);
1233
- (0, import_react.useEffect)(() => {
1234
- if (!wantsArtifacts) return;
1235
- let alive = true;
1236
- let retries = 0;
1237
- let timer;
1238
- const load = () => {
1239
- getArtifacts({ data: { loopId } }).then((list) => alive && setArtifacts(list)).catch(() => {
1240
- if (alive && retries < ARTIFACT_RETRY_MAX) timer = setTimeout(load, ARTIFACT_RETRY_MS * ++retries);
1241
- });
1242
- };
1243
- load();
1244
- return () => {
1245
- alive = false;
1246
- clearTimeout(timer);
1247
- };
1248
- }, [
1249
- wantsArtifacts,
1250
- loopId,
1251
- newestRunId,
1252
- newestRunLive
1253
- ]);
1254
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1255
- className: "loopview text-[14px] leading-relaxed",
1256
- children: esm_default(clean, (0, import_react.useMemo)(() => ({ replace: (node) => {
1257
- if (!(node instanceof import_lib.Element)) return void 0;
1258
- const a = node.attribs ?? {};
1259
- if (node.name === "loop-chart") {
1260
- const parsed = parseChartSpec(a);
1261
- if (!parsed.ok) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1262
- className: "rounded-control border border-[var(--color-wire)] bg-[var(--color-raised)] p-3 text-meta text-[var(--color-secondary)]",
1263
- children: "This chart uses an outdated or invalid configuration."
1264
- });
1265
- return chartRuns ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopChart, {
1266
- runs: chartRuns,
1267
- spec: parsed.value
1268
- }) : null;
1269
- }
1270
- if (node.name === "loop-embed") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopEmbed, {
1271
- loopId,
1272
- artifacts,
1273
- file: a.file,
1274
- match: a.match,
1275
- full: "full" in a,
1276
- taskFile
1277
- });
1278
- if (node.name === "loop-calendar") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopCalendar, {
1279
- loopId,
1280
- artifacts,
1281
- match: a.match,
1282
- taskFile
1283
- });
1284
- if (node.name === "loop-kanban") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopKanban, {
1285
- loopId,
1286
- artifacts,
1287
- columns: a.columns,
1288
- match: a.match,
1289
- taskFile
1290
- });
1291
- } }), [
1292
- chartRuns,
1293
- loopId,
1294
- taskFile,
1295
- artifacts
1296
- ]))
1297
- });
1298
- }
1299
- //#endregion
1300
- export { LoopView };