@kky42/pievo-server 1.0.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.
Files changed (197) hide show
  1. package/.output/nitro.json +17 -0
  2. package/.output/public/assets/CodeField-dOzyFQNH.js +34 -0
  3. package/.output/public/assets/LoopView-B4H9QibC.js +55 -0
  4. package/.output/public/assets/Modal-D33gNSgx.js +1 -0
  5. package/.output/public/assets/SignIn-SHuRRyOv.js +1 -0
  6. package/.output/public/assets/actionUi-ChGGALzg.js +1 -0
  7. package/.output/public/assets/app-DElSo_jy.css +2 -0
  8. package/.output/public/assets/artifactView-DFuubXLJ.js +66 -0
  9. package/.output/public/assets/index-DfpTWRSz.js +12 -0
  10. package/.output/public/assets/invite._token-DJtd-XE2.js +1 -0
  11. package/.output/public/assets/invite._token-T8oo4Qtw.js +1 -0
  12. package/.output/public/assets/jsx-runtime-BseJUIpC.js +1 -0
  13. package/.output/public/assets/lifecycleUi-DKkChdnU.js +1 -0
  14. package/.output/public/assets/loopApi-BM2nj71x.js +3 -0
  15. package/.output/public/assets/loops._loopId-B19Jw7jC.js +5 -0
  16. package/.output/public/assets/loops._loopId_.runs._runId-_CflHV38.js +3 -0
  17. package/.output/public/assets/notifyFns-DCubjpRy.js +1 -0
  18. package/.output/public/assets/preload-helper-CKUiEq3b.js +3 -0
  19. package/.output/public/assets/routes-B9tgli5f.js +1 -0
  20. package/.output/public/assets/routes-CTtlM_qt.js +1 -0
  21. package/.output/public/assets/t._teamId-Bm21mngW.js +1 -0
  22. package/.output/public/assets/t._teamId-gR7BVKkR.js +1 -0
  23. package/.output/public/assets/ui-Dp88qUWh.js +1 -0
  24. package/.output/public/assets/useRouter-DsjH8G0p.js +1 -0
  25. package/.output/public/favicon.svg +12 -0
  26. package/.output/public/fonts/GeistPixel-Square.woff2 +0 -0
  27. package/.output/server/_chunks/ssr-renderer.mjs +26 -0
  28. package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +419 -0
  29. package/.output/server/_libs/@aws-sdk/checksums+[...].mjs +10732 -0
  30. package/.output/server/_libs/@aws-sdk/client-s3+[...].mjs +9475 -0
  31. package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +137 -0
  32. package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +440 -0
  33. package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +62 -0
  34. package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +818 -0
  35. package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +59 -0
  36. package/.output/server/_libs/@base-ui/react+[...].mjs +11125 -0
  37. package/.output/server/_libs/@codemirror/autocomplete+[...].mjs +19713 -0
  38. package/.output/server/_libs/@codemirror/lang-css+[...].mjs +2636 -0
  39. package/.output/server/_libs/@codemirror/lang-html+[...].mjs +2247 -0
  40. package/.output/server/_libs/@codemirror/lang-json+[...].mjs +79 -0
  41. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1736 -0
  42. package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +342 -0
  43. package/.output/server/_libs/@tanstack/react-router+[...].mjs +14300 -0
  44. package/.output/server/_libs/@uiw/codemirror-extensions-basic-setup+[...].mjs +76 -0
  45. package/.output/server/_libs/atomic-sleep.mjs +27 -0
  46. package/.output/server/_libs/aws-crypto__crc32c.mjs +314 -0
  47. package/.output/server/_libs/aws-sdk__nested-clients.mjs +2671 -0
  48. package/.output/server/_libs/babel__runtime.mjs +30 -0
  49. package/.output/server/_libs/clsx.mjs +16 -0
  50. package/.output/server/_libs/codemirror__commands.mjs +1725 -0
  51. package/.output/server/_libs/codemirror__lint.mjs +565 -0
  52. package/.output/server/_libs/codemirror__search.mjs +1138 -0
  53. package/.output/server/_libs/codemirror__theme-one-dark.mjs +161 -0
  54. package/.output/server/_libs/colorette.mjs +129 -0
  55. package/.output/server/_libs/croner.mjs +735 -0
  56. package/.output/server/_libs/d3-array.mjs +231 -0
  57. package/.output/server/_libs/d3-color.mjs +330 -0
  58. package/.output/server/_libs/d3-format.mjs +252 -0
  59. package/.output/server/_libs/d3-interpolate.mjs +163 -0
  60. package/.output/server/_libs/d3-path.mjs +77 -0
  61. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  62. package/.output/server/_libs/d3-shape.mjs +689 -0
  63. package/.output/server/_libs/dateformat.mjs +341 -0
  64. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  65. package/.output/server/_libs/diff.mjs +732 -0
  66. package/.output/server/_libs/dom-serializer+[...].mjs +708 -0
  67. package/.output/server/_libs/domhandler.mjs +461 -0
  68. package/.output/server/_libs/dompurify.mjs +2111 -0
  69. package/.output/server/_libs/domutils.mjs +868 -0
  70. package/.output/server/_libs/drizzle-orm+postgres.mjs +7689 -0
  71. package/.output/server/_libs/electric-sql__pglite.mjs +14674 -0
  72. package/.output/server/_libs/end-of-stream+once+wrappy.mjs +145 -0
  73. package/.output/server/_libs/es-toolkit.mjs +1737 -0
  74. package/.output/server/_libs/eventemitter3.mjs +236 -0
  75. package/.output/server/_libs/fast-copy.mjs +285 -0
  76. package/.output/server/_libs/fast-safe-stringify.mjs +172 -0
  77. package/.output/server/_libs/h3+rou3+srvx.mjs +1201 -0
  78. package/.output/server/_libs/html-dom-parser+htmlparser2.mjs +1560 -0
  79. package/.output/server/_libs/html-react-parser+[...].mjs +1360 -0
  80. package/.output/server/_libs/initdb.wasm +0 -0
  81. package/.output/server/_libs/marked.mjs +1548 -0
  82. package/.output/server/_libs/on-exit-leak-free.mjs +80 -0
  83. package/.output/server/_libs/pglite.data +37667 -8
  84. package/.output/server/_libs/pglite.wasm +0 -0
  85. package/.output/server/_libs/pino+[...].mjs +3196 -0
  86. package/.output/server/_libs/pino-abstract-transport+split2.mjs +194 -0
  87. package/.output/server/_libs/pino-pretty+[...].mjs +1623 -0
  88. package/.output/server/_libs/pinojs__redact.mjs +280 -0
  89. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  90. package/.output/server/_libs/recharts+victory-vendor.mjs +15875 -0
  91. package/.output/server/_libs/tanstack__history.mjs +342 -0
  92. package/.output/server/_libs/ufo.mjs +64 -0
  93. package/.output/server/_libs/uiw__codemirror-themes.mjs +32 -0
  94. package/.output/server/_libs/uiw__react-codemirror.mjs +354 -0
  95. package/.output/server/_runtime.mjs +37 -0
  96. package/.output/server/_ssr/CodeField-tm4rr6G3.mjs +143 -0
  97. package/.output/server/_ssr/DashboardView-CC7htv28.mjs +2795 -0
  98. package/.output/server/_ssr/LoopPlaybook-BXH8mhiX.mjs +2105 -0
  99. package/.output/server/_ssr/LoopView-DCugEbHx.mjs +1106 -0
  100. package/.output/server/_ssr/Modal-DpKZHLZK.mjs +49 -0
  101. package/.output/server/_ssr/SignIn-dTndU6Hw.mjs +67 -0
  102. package/.output/server/_ssr/actionUi-CZbEixbf.mjs +119 -0
  103. package/.output/server/_ssr/adapters-BS8XosjF.mjs +130 -0
  104. package/.output/server/_ssr/artifactFiles-Cagtj7J0.mjs +61 -0
  105. package/.output/server/_ssr/artifactKind-BW0isV8L.mjs +37 -0
  106. package/.output/server/_ssr/artifactView-DS4O22wl.mjs +304 -0
  107. package/.output/server/_ssr/artifacts-DngKyW2u.mjs +2 -0
  108. package/.output/server/_ssr/auth-DY1DuYZd.mjs +38429 -0
  109. package/.output/server/_ssr/boot-BbtG0ImW.mjs +9010 -0
  110. package/.output/server/_ssr/boot-CMHTxQpJ.mjs +2 -0
  111. package/.output/server/_ssr/bun-sqlite-dialect-BW9W1_Ps-CU1Spd6z.mjs +162 -0
  112. package/.output/server/_ssr/core-RrmWoKne.mjs +5 -0
  113. package/.output/server/_ssr/createServerRpc-BD8Xn_0_.mjs +12 -0
  114. package/.output/server/_ssr/d1-sqlite-dialect-BLC8LXE6-DfG7tB8s.mjs +112 -0
  115. package/.output/server/_ssr/db-DWAuZq1o.mjs +2 -0
  116. package/.output/server/_ssr/dist-BIxFFVZg.mjs +342 -0
  117. package/.output/server/_ssr/empty-plugin-adapters-DKUlnwwl.mjs +5 -0
  118. package/.output/server/_ssr/factory-Blq0DYfT.mjs +6903 -0
  119. package/.output/server/_ssr/invite._token-BRCo9LGB.mjs +36 -0
  120. package/.output/server/_ssr/invite._token-DJxGLILJ.mjs +28 -0
  121. package/.output/server/_ssr/invite._token-NewFt27g.mjs +92 -0
  122. package/.output/server/_ssr/kysely-adapter-D3XTe64J.mjs +2 -0
  123. package/.output/server/_ssr/kysely-migration-tables-JkVUjPF_-bPAX9LR_.mjs +19 -0
  124. package/.output/server/_ssr/lifecycleUi-C9lg1r3r.mjs +38 -0
  125. package/.output/server/_ssr/logger-Dw82jNjH.mjs +2 -0
  126. package/.output/server/_ssr/loopApi-BtfkZQYK.mjs +532 -0
  127. package/.output/server/_ssr/loopApi-Cxe8WyOB.mjs +105 -0
  128. package/.output/server/_ssr/loops._loopId-7NQJPB9W.mjs +30 -0
  129. package/.output/server/_ssr/loops._loopId-DhnArGdL.mjs +1635 -0
  130. package/.output/server/_ssr/loops._loopId_.runs._runId-Bvuo2nki.mjs +29 -0
  131. package/.output/server/_ssr/loops._loopId_.runs._runId-Dip5Xk-q.mjs +674 -0
  132. package/.output/server/_ssr/machineFns-Dvr5ICUy.mjs +266 -0
  133. package/.output/server/_ssr/node-sqlite-dialect-DhyOpJTX.mjs +162 -0
  134. package/.output/server/_ssr/notifyFns-BcMRY5kP.mjs +118 -0
  135. package/.output/server/_ssr/notifyFns-Q458pZmE.mjs +580 -0
  136. package/.output/server/_ssr/router-J8KZ30xw.mjs +678 -0
  137. package/.output/server/_ssr/routes-Bcs4T1ox.mjs +28 -0
  138. package/.output/server/_ssr/routes-BoGrcf-b.mjs +43 -0
  139. package/.output/server/_ssr/routes-D4am3Zff.mjs +28 -0
  140. package/.output/server/_ssr/runDiff-CpEFqo7G.mjs +113 -0
  141. package/.output/server/_ssr/ssr.mjs +5837 -0
  142. package/.output/server/_ssr/start-4ifArtKx.mjs +4 -0
  143. package/.output/server/_ssr/store-b3JcQ08F.mjs +2 -0
  144. package/.output/server/_ssr/t._teamId-CeUhzgzT.mjs +32 -0
  145. package/.output/server/_ssr/t._teamId-DR-hQK7o.mjs +47 -0
  146. package/.output/server/_ssr/t._teamId-Dy_McbRt.mjs +31 -0
  147. package/.output/server/_ssr/teamCookie-B7hLOIie.mjs +10 -0
  148. package/.output/server/_ssr/teamFns-BIDnsg2A.mjs +473 -0
  149. package/.output/server/_ssr/teamFns-DA4ovI5g.mjs +33 -0
  150. package/.output/server/_ssr/tokens-ytd60FUt.mjs +2 -0
  151. package/.output/server/_ssr/ui-Dej2qjuS.mjs +267 -0
  152. package/.output/server/_tanstack-start-manifest_v-CVS6c7re.mjs +95 -0
  153. package/.output/server/drizzle/0000_round_marauders.sql +193 -0
  154. package/.output/server/drizzle/0001_naive_silver_fox.sql +25 -0
  155. package/.output/server/drizzle/0002_funny_sage.sql +12 -0
  156. package/.output/server/drizzle/0003_optimal_slapstick.sql +102 -0
  157. package/.output/server/drizzle/0004_remove_grok_agent.sql +5 -0
  158. package/.output/server/drizzle/0005_heavy_gauntlet.sql +8 -0
  159. package/.output/server/drizzle/0006_tranquil_sharon_carter.sql +1 -0
  160. package/.output/server/drizzle/0007_faulty_starbolt.sql +30 -0
  161. package/.output/server/drizzle/0008_durable_lifecycle_evidence.sql +15 -0
  162. package/.output/server/drizzle/meta/0000_snapshot.json +1333 -0
  163. package/.output/server/drizzle/meta/0001_snapshot.json +1498 -0
  164. package/.output/server/drizzle/meta/0002_snapshot.json +1576 -0
  165. package/.output/server/drizzle/meta/0003_snapshot.json +1666 -0
  166. package/.output/server/drizzle/meta/0005_snapshot.json +1666 -0
  167. package/.output/server/drizzle/meta/0006_snapshot.json +1672 -0
  168. package/.output/server/drizzle/meta/0007_snapshot.json +1787 -0
  169. package/.output/server/drizzle/meta/_journal.json +69 -0
  170. package/.output/server/index.mjs +464 -0
  171. package/.output/server/node_modules/tslib/modules/index.js +70 -0
  172. package/.output/server/node_modules/tslib/modules/package.json +3 -0
  173. package/.output/server/node_modules/tslib/package.json +47 -0
  174. package/.output/server/node_modules/tslib/tslib.js +484 -0
  175. package/.output/server/package.json +9 -0
  176. package/LICENSE +21 -0
  177. package/drizzle/0000_round_marauders.sql +193 -0
  178. package/drizzle/0001_naive_silver_fox.sql +25 -0
  179. package/drizzle/0002_funny_sage.sql +12 -0
  180. package/drizzle/0003_optimal_slapstick.sql +102 -0
  181. package/drizzle/0004_remove_grok_agent.sql +5 -0
  182. package/drizzle/0005_heavy_gauntlet.sql +8 -0
  183. package/drizzle/0006_tranquil_sharon_carter.sql +1 -0
  184. package/drizzle/0007_faulty_starbolt.sql +30 -0
  185. package/drizzle/0008_durable_lifecycle_evidence.sql +15 -0
  186. package/drizzle/meta/0000_snapshot.json +1333 -0
  187. package/drizzle/meta/0001_snapshot.json +1498 -0
  188. package/drizzle/meta/0002_snapshot.json +1576 -0
  189. package/drizzle/meta/0003_snapshot.json +1666 -0
  190. package/drizzle/meta/0005_snapshot.json +1666 -0
  191. package/drizzle/meta/0006_snapshot.json +1672 -0
  192. package/drizzle/meta/0007_snapshot.json +1787 -0
  193. package/drizzle/meta/_journal.json +69 -0
  194. package/package.json +84 -0
  195. package/scripts/pievo-server.mjs +390 -0
  196. package/scripts/prestart.mjs +66 -0
  197. package/scripts/server-cli-lib.mjs +451 -0
@@ -0,0 +1,1106 @@
1
+ import { o as __toESM } from "../_runtime.mjs";
2
+ import { C as require_jsx_runtime, S as require_react } from "../_libs/@base-ui/react+[...].mjs";
3
+ import { pt as isTaskPath } from "./boot-BbtG0ImW.mjs";
4
+ import { T as md, _ as fmt, j as tsShort, v as fnum } from "./ui-Dej2qjuS.mjs";
5
+ import { c as getArtifacts } from "./loopApi-Cxe8WyOB.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-DS4O22wl.mjs";
9
+ import { n as ModalHead, t as Modal } from "./Modal-DpKZHLZK.mjs";
10
+ import { n as import_lib, t as esm_default } from "../_libs/html-react-parser+[...].mjs";
11
+ import { a as Area, c as Tooltip, i as XAxis, l as ResponsiveContainer, n as LineChart, o as Line, r as YAxis, s as CartesianGrid, t as AreaChart } from "../_libs/recharts+victory-vendor.mjs";
12
+ //#region node_modules/.nitro/vite/services/ssr/assets/LoopView-DCugEbHx.js
13
+ var import_react = /* @__PURE__ */ __toESM(require_react());
14
+ var import_jsx_runtime = require_jsx_runtime();
15
+ /** Parse a `<loop-chart series="key:label:unit, ...">` attribute into StateField[]. */
16
+ function parseSeries(attr) {
17
+ if (!attr) return [];
18
+ return attr.split(",").map((s) => {
19
+ const [key, label, unit] = s.split(":").map((x) => x.trim());
20
+ return {
21
+ key: key ?? "",
22
+ ...label ? { label } : {},
23
+ ...unit ? { unit } : {}
24
+ };
25
+ }).filter((f) => f.key);
26
+ }
27
+ function buildBindingContext(runs) {
28
+ const latest = {};
29
+ for (const r of runs) if (r.state && typeof r.state === "object" && !Array.isArray(r.state)) {
30
+ for (const k in r.state) if (!(k in latest)) {
31
+ const v = r.state[k];
32
+ if (typeof v === "number") latest[k] = fnum(v);
33
+ else if (typeof v === "string") latest[k] = v;
34
+ }
35
+ }
36
+ return { latest };
37
+ }
38
+ var TOKEN = /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g;
39
+ var escapeHtml = (s) => s.replace(/[&<>"]/g, (c) => ({
40
+ "&": "&amp;",
41
+ "<": "&lt;",
42
+ ">": "&gt;",
43
+ "\"": "&quot;"
44
+ })[c] ?? c);
45
+ /** Interpolate `{{...}}` tokens. Values are HTML-escaped (they are agent-controlled data). */
46
+ function resolveBindings(html, ctx) {
47
+ return html.replace(TOKEN, (_m, path) => {
48
+ const v = lookup(ctx, path);
49
+ return v == null ? "—" : escapeHtml(v);
50
+ });
51
+ }
52
+ function lookup(ctx, path) {
53
+ const parts = path.split(".");
54
+ if (parts[0] === "latest" && parts[1]) return ctx.latest[parts[1]];
55
+ }
56
+ /** Per-run numeric state snapshots → one series per numeric field (chronological). */
57
+ function numericSeries(runsNewestFirst) {
58
+ const chron = (runsNewestFirst ?? []).slice().reverse();
59
+ const series = {};
60
+ for (const r of chron) {
61
+ const obj = r.state && typeof r.state === "object" && !Array.isArray(r.state) ? r.state : {};
62
+ for (const k in obj) {
63
+ const v = obj[k];
64
+ if (typeof v === "number" && isFinite(v)) (series[k] ??= []).push({
65
+ t: r.ts,
66
+ v
67
+ });
68
+ }
69
+ }
70
+ return series;
71
+ }
72
+ /**
73
+ * Merge per-key series into the row-oriented array Recharts consumes
74
+ * (`[{__t, k1, k2}, …]`, chronological). Rows are the union of the requested
75
+ * keys' timestamps - a run that reported only some keys leaves the others
76
+ * absent on that row (the chart bridges the gap via `connectNulls`).
77
+ */
78
+ function seriesRows(data, keys) {
79
+ const ts = /* @__PURE__ */ new Set();
80
+ for (const k of keys) for (const p of data[k] ?? []) ts.add(p.t);
81
+ const sorted = [...ts].sort();
82
+ const idx = new Map(sorted.map((t, i) => [t, i]));
83
+ const rows = sorted.map((t) => ({ __t: t }));
84
+ for (const k of keys) for (const p of data[k] ?? []) rows[idx.get(p.t)][k] = p.v;
85
+ return rows;
86
+ }
87
+ /**
88
+ * `<loop-chart series="mrr:MRR:$, paid:Paid">` - the loop's run-history trend,
89
+ * rendered by Recharts in the shadcn/ui-charts grammar, themed entirely by the
90
+ * theme tokens (`--color-chart-1..5` ramp, hairline grid, mono ticks,
91
+ * flat tooltip - no shadows, no decorative color). One series renders as a
92
+ * gradient area; multiple series render as plain lines (overlapping translucent
93
+ * fills go muddy in a monochrome palette). A single-point series renders a dot
94
+ * instead of nothing, so a young loop's dashboard is never blank.
95
+ *
96
+ * `data` is the shared `numericSeries(runs)` map computed once by LoopView.
97
+ */
98
+ /** Distinct strokes for multiple series - display ink first, then the signal colors. */
99
+ var STROKES = [
100
+ "var(--color-chart-1)",
101
+ "var(--color-chart-2)",
102
+ "var(--color-chart-3)",
103
+ "var(--color-chart-4)",
104
+ "var(--color-chart-5)"
105
+ ];
106
+ /**
107
+ * Fixed pixel height: ResponsiveContainer tracks the CONTAINER width while the
108
+ * height stays constant - the fix for the old stretched-viewBox renderer, which
109
+ * scaled like an image (≈4px-fat strokes and a ~340px-tall chart at the loop
110
+ * page's full dashboard width). Also the initial render size before the
111
+ * ResizeObserver measures (and the only size in a non-browser render).
112
+ */
113
+ var HEIGHT = 190;
114
+ var INITIAL = {
115
+ width: 640,
116
+ height: HEIGHT
117
+ };
118
+ var TICK = {
119
+ fontSize: 10,
120
+ fontFamily: "var(--font-mono)",
121
+ fill: "var(--color-secondary)"
122
+ };
123
+ var TOOLTIP_STYLE = {
124
+ background: "var(--color-surface)",
125
+ border: "1px solid var(--color-wire)",
126
+ borderRadius: 8,
127
+ boxShadow: "none",
128
+ padding: "6px 10px",
129
+ fontFamily: "var(--font-mono)",
130
+ fontSize: 11
131
+ };
132
+ /** "$12.4k" / "40℃" / "7" - the unit placement the legend has always used. */
133
+ var withUnit = (v, unit) => unit === "$" ? `$${fnum(v)}` : `${fnum(v)}${unit}`;
134
+ function LoopChart({ data, series }) {
135
+ const gradientId = (0, import_react.useId)();
136
+ if (!series.length) return null;
137
+ const plotted = series.map((f) => ({
138
+ field: f,
139
+ pts: data[f.key] ?? []
140
+ })).filter((s) => s.pts.length >= 1);
141
+ if (!plotted.length) return null;
142
+ const rows = seriesRows(data, plotted.map((s) => s.field.key));
143
+ const single = plotted.length === 1;
144
+ const unitOf = new Map(plotted.map((s) => [s.field.key, s.field.unit ?? ""]));
145
+ const xAxis = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XAxis, {
146
+ dataKey: "__t",
147
+ tickLine: false,
148
+ axisLine: false,
149
+ tickMargin: 8,
150
+ minTickGap: 28,
151
+ interval: "preserveStartEnd",
152
+ tick: TICK,
153
+ tickFormatter: (t) => md(t)
154
+ });
155
+ const yAxis = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(YAxis, {
156
+ width: 34,
157
+ tickCount: 4,
158
+ tickLine: false,
159
+ axisLine: false,
160
+ tick: TICK,
161
+ tickFormatter: (v) => fnum(v)
162
+ });
163
+ const grid = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CartesianGrid, {
164
+ vertical: false,
165
+ stroke: "var(--color-hairline)"
166
+ });
167
+ const tooltip = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip, {
168
+ isAnimationActive: false,
169
+ cursor: {
170
+ stroke: "var(--color-wire)",
171
+ strokeDasharray: "3 3"
172
+ },
173
+ contentStyle: TOOLTIP_STYLE,
174
+ labelStyle: {
175
+ color: "var(--color-secondary)",
176
+ marginBottom: 2
177
+ },
178
+ itemStyle: {
179
+ color: "var(--color-primary)",
180
+ padding: "1px 0"
181
+ },
182
+ labelFormatter: (t) => tsShort(String(t)),
183
+ formatter: (v, name, item) => [withUnit(Number(v), unitOf.get(String(item?.dataKey)) ?? ""), String(name)]
184
+ });
185
+ const seriesProps = (s, idx) => {
186
+ const color = STROKES[idx % STROKES.length];
187
+ return {
188
+ dataKey: s.field.key,
189
+ name: s.field.label ?? s.field.key,
190
+ type: "monotone",
191
+ stroke: color,
192
+ strokeWidth: 1.5,
193
+ dot: s.pts.length === 1 ? {
194
+ r: 3.5,
195
+ strokeWidth: 0,
196
+ fill: color
197
+ } : false,
198
+ activeDot: {
199
+ r: 3,
200
+ strokeWidth: 0,
201
+ fill: color
202
+ },
203
+ connectNulls: true,
204
+ isAnimationActive: false
205
+ };
206
+ };
207
+ const margin = {
208
+ top: 6,
209
+ right: 12,
210
+ left: 0,
211
+ bottom: 0
212
+ };
213
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figure", {
214
+ className: "my-2 min-w-0",
215
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
216
+ className: "min-w-0",
217
+ style: { height: HEIGHT },
218
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResponsiveContainer, {
219
+ width: "100%",
220
+ height: "100%",
221
+ initialDimension: INITIAL,
222
+ children: single ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AreaChart, {
223
+ data: rows,
224
+ margin,
225
+ children: [
226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", {
227
+ id: gradientId,
228
+ x1: "0",
229
+ y1: "0",
230
+ x2: "0",
231
+ y2: "1",
232
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
233
+ offset: "5%",
234
+ stopColor: STROKES[0],
235
+ stopOpacity: .28
236
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
237
+ offset: "95%",
238
+ stopColor: STROKES[0],
239
+ stopOpacity: .02
240
+ })]
241
+ }) }),
242
+ grid,
243
+ xAxis,
244
+ yAxis,
245
+ tooltip,
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Area, {
247
+ ...seriesProps(plotted[0], 0),
248
+ fill: `url(#${gradientId})`
249
+ })
250
+ ]
251
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(LineChart, {
252
+ data: rows,
253
+ margin,
254
+ children: [
255
+ grid,
256
+ xAxis,
257
+ yAxis,
258
+ tooltip,
259
+ plotted.map((s, idx) => /* @__PURE__ */ (0, import_react.createElement)(Line, {
260
+ ...seriesProps(s, idx),
261
+ key: s.field.key
262
+ }))
263
+ ]
264
+ })
265
+ })
266
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("figcaption", {
267
+ className: "mt-1 flex flex-wrap gap-x-4 gap-y-1 text-label text-[var(--color-secondary)]",
268
+ children: plotted.map((s, idx) => {
269
+ const last = s.pts[s.pts.length - 1];
270
+ if (!last) return null;
271
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
272
+ className: "inline-flex items-center gap-1.5",
273
+ children: [
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
275
+ className: "inline-block h-[2px] w-3 align-middle",
276
+ style: { background: STROKES[idx % STROKES.length] }
277
+ }),
278
+ s.field.label ?? s.field.key,
279
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
280
+ className: "text-[var(--color-display)]",
281
+ children: withUnit(last.v, s.field.unit ?? "")
282
+ })
283
+ ]
284
+ }, s.field.key);
285
+ })
286
+ })]
287
+ });
288
+ }
289
+ var basename$2 = (path) => path.split("/").pop() || path;
290
+ var pad = (n) => String(n).padStart(2, "0");
291
+ /** A real calendar date within a sane range (no 2026-13-40, no year 10521). */
292
+ function validDate(y, m, d) {
293
+ if (y < 1970 || y > 2100 || m < 1 || m > 12 || d < 1 || d > 31) return false;
294
+ const dt = new Date(Date.UTC(y, m - 1, d));
295
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
296
+ }
297
+ var DATE_RE = /(\d{4})(?:([-_])(\d{2})\2(\d{2})|(\d{2})(\d{2}))/g;
298
+ /** First valid date found in the file's basename, else null. */
299
+ function parseFilenameDate(path) {
300
+ const name = basename$2(path);
301
+ DATE_RE.lastIndex = 0;
302
+ for (let m = DATE_RE.exec(name); m; m = DATE_RE.exec(name)) {
303
+ const before = name[m.index - 1];
304
+ const after = name[m.index + m[0].length];
305
+ if (before && /\d/.test(before) || after && /\d/.test(after)) continue;
306
+ const y = Number(m[1]);
307
+ const mo = Number(m[3] ?? m[5]);
308
+ const d = Number(m[4] ?? m[6]);
309
+ if (validDate(y, mo, d)) return `${m[1]}-${pad(mo)}-${pad(d)}`;
310
+ }
311
+ return null;
312
+ }
313
+ /** Local calendar day of an ISO timestamp (the calendar renders user-local). */
314
+ function localDay(iso) {
315
+ const d = new Date(iso);
316
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
317
+ }
318
+ /**
319
+ * A calendar day from a front-matter `date:` value (stored RAW — validity is
320
+ * decided here). Forgiving about the shape: takes the leading `YYYY-MM-DD` (or
321
+ * `/`/`_` separators, or a full ISO timestamp's date part) and validates it's a
322
+ * real day. Returns null for anything unparseable/invalid (fall through to
323
+ * filename/sync).
324
+ */
325
+ function parseMetaDate(raw) {
326
+ if (typeof raw !== "string") return null;
327
+ const m = /^\s*(\d{4})[-/_](\d{2})[-/_](\d{2})/.exec(raw);
328
+ if (!m) return null;
329
+ const y = Number(m[1]);
330
+ const mo = Number(m[2]);
331
+ const d = Number(m[3]);
332
+ return validDate(y, mo, d) ? `${m[1]}-${pad(mo)}-${pad(d)}` : null;
333
+ }
334
+ /** Date one artifact: front-matter `date:` first (authoritative), filename next,
335
+ * sync time as the marked fallback. */
336
+ function productDate(file) {
337
+ const fromMeta = parseMetaDate(file.meta?.date);
338
+ if (fromMeta) return {
339
+ date: fromMeta,
340
+ source: "frontmatter"
341
+ };
342
+ const fromName = parseFilenameDate(file.path);
343
+ return fromName ? {
344
+ date: fromName,
345
+ source: "filename"
346
+ } : {
347
+ date: localDay(file.updatedAt),
348
+ source: "sync"
349
+ };
350
+ }
351
+ /**
352
+ * `match` glob → RegExp. `*` matches within a path segment (never `/`); all
353
+ * other characters are literal. A pattern with no `/` matches against the
354
+ * BASENAME (so `*.md` means "any markdown file, anywhere"), a pattern with
355
+ * one matches the whole loop-relative path.
356
+ */
357
+ function globToRegExp(pattern) {
358
+ const rx = pattern.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
359
+ return new RegExp(`^${rx}$`);
360
+ }
361
+ /** The artifacts a `match` pattern selects (no pattern ⇒ all). */
362
+ function matchArtifacts(files, pattern) {
363
+ if (!pattern) return files;
364
+ const rx = globToRegExp(pattern);
365
+ const onBase = !pattern.includes("/");
366
+ return files.filter((f) => rx.test(onBase ? basename$2(f.path) : f.path));
367
+ }
368
+ /**
369
+ * The newest artifact a pattern selects - by product date (filename first)
370
+ * descending, sync time as the tiebreak. This keeps "the newest digest"
371
+ * correct even when an OLD file happens to re-sync last.
372
+ */
373
+ function newestMatch(files, pattern) {
374
+ const matched = matchArtifacts(files, pattern);
375
+ let best;
376
+ let bestKey = "";
377
+ for (const f of matched) {
378
+ const key = `${productDate(f).date}|${f.updatedAt}`;
379
+ if (!best || key > bestKey) {
380
+ best = f;
381
+ bestKey = key;
382
+ }
383
+ }
384
+ return best;
385
+ }
386
+ /**
387
+ * `<loop-embed file="reports/digest-2026-07-01.md">` /
388
+ * `<loop-embed match="reports/digest-*.md">` - a loop's own produced file
389
+ * embedded in its dashboard. `file` targets one exact path; `match` resolves to
390
+ * the NEWEST matching artifact (filename date first, sync time as tiebreak), so
391
+ * "embed the latest digest" keeps working without the agent editing the
392
+ * template every run. `match` never selects the loop's task file - the spec is
393
+ * not a product (same rule as the calendar's default set), and its frequent
394
+ * edit-syncs would pin it as "newest" under a broad glob like `*.md`; an exact
395
+ * `file=` path may still target it. Content renders through the shared
396
+ * artifact viewer (markdown via .taskmd, mono pre for other text, download
397
+ * notice for binary), collapsed at 300px with a fade + "show all" toggle; the
398
+ * `full` attribute opts out of the collapse.
399
+ */
400
+ /** Collapsed preview height. A pixel cap, not a line clamp - markdown blocks
401
+ * (tables, code) have uneven line heights, so pixels are the only stable cut. */
402
+ var COLLAPSE_PX = 300;
403
+ var norm = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
404
+ function LoopEmbed({ loopId, artifacts, file, match, full, taskFile }) {
405
+ const requested = file ?? match;
406
+ const shell = "min-w-0 overflow-hidden rounded-card border border-hairline bg-surface shadow-card";
407
+ if (!requested) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
408
+ className: shell,
409
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
410
+ className: "px-5 py-5 font-mono text-label text-disabled",
411
+ children: "<loop-embed> needs file=\"<path>\" or match=\"<glob>\""
412
+ })
413
+ });
414
+ if (artifacts == null) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
415
+ className: shell,
416
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewerHead, { path: requested }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
417
+ className: "px-5 py-6 text-label text-secondary",
418
+ children: "Loading…"
419
+ })]
420
+ });
421
+ const matchable = file ? artifacts : artifacts.filter((a) => !isTaskPath(taskFile, a.path));
422
+ const target = file ? artifacts.find((a) => norm(a.path) === norm(file)) : newestMatch(matchable, match);
423
+ if (!target) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
424
+ className: shell,
425
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewerHead, { path: requested }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
426
+ className: "px-5 py-6 text-body text-disabled",
427
+ children: "No synced file matches yet. It appears here after the loop's next run syncs it."
428
+ })]
429
+ });
430
+ const meta = `${match ? `newest of ${matchArtifacts(matchable, match).length} matching ${match} · ` : ""}synced ${fmt(target.updatedAt)}`;
431
+ const kind = artifactKind(target.path);
432
+ const collapse = !full && !(kind === "html" || kind === "image") && !target.binary && !target.oversize;
433
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
434
+ className: shell,
435
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewerHead, {
436
+ path: target.path,
437
+ meta,
438
+ action: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
439
+ href: "#files",
440
+ className: "text-caption font-medium text-interactive transition-colors hover:text-display",
441
+ children: "Open in files →"
442
+ })
443
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleBody, {
444
+ collapse,
445
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArtifactBody, {
446
+ loopId,
447
+ file: target
448
+ })
449
+ })]
450
+ });
451
+ }
452
+ /**
453
+ * The collapse wrapper: clipping happens on THIS wrapper (overflow-hidden +
454
+ * max-height), never on the text nodes, with a fade overlay above the cut and
455
+ * the toggle in its own footer. The toggle appears only when the content
456
+ * actually overflows the cap (measured post-render, re-checked as the lazy
457
+ * content lands).
458
+ */
459
+ function CollapsibleBody({ collapse, children }) {
460
+ const ref = (0, import_react.useRef)(null);
461
+ const [expanded, setExpanded] = (0, import_react.useState)(false);
462
+ const [overflowing, setOverflowing] = (0, import_react.useState)(false);
463
+ (0, import_react.useLayoutEffect)(() => {
464
+ const el = ref.current;
465
+ if (!el) return;
466
+ const over = el.scrollHeight > 301;
467
+ if (over !== overflowing) setOverflowing(over);
468
+ });
469
+ const collapsed = collapse && !expanded && overflowing;
470
+ if (!collapse) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
471
+ className: "min-w-0",
472
+ children
473
+ });
474
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
475
+ className: "relative min-w-0",
476
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
477
+ ref,
478
+ className: collapsed ? "overflow-hidden" : "",
479
+ style: collapsed ? { maxHeight: COLLAPSE_PX } : void 0,
480
+ children
481
+ }), collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
482
+ "aria-hidden": true,
483
+ className: "pointer-events-none absolute inset-x-0 bottom-0 h-16",
484
+ style: { background: "linear-gradient(to bottom, transparent, var(--color-surface))" }
485
+ })]
486
+ }), overflowing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
487
+ className: "flex border-t border-hairline px-3.5 py-1.5",
488
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
489
+ type: "button",
490
+ onClick: () => setExpanded(!expanded),
491
+ className: "cursor-pointer border-none bg-transparent p-0 text-caption font-medium text-secondary transition-colors hover:text-display",
492
+ children: expanded ? "Collapse ▴" : "Show all ▾"
493
+ })
494
+ })] });
495
+ }
496
+ /**
497
+ * `<loop-calendar match="reports/*.md">` - the loop's products on a month
498
+ * grid. Each product lands on the day parsed from its FILENAME (ISO-ish
499
+ * patterns), falling back to its sync time (marked with a dashed chip - sync
500
+ * time can misattribute a re-synced old file, so the fallback is visible, not
501
+ * silent). Clicking a product reviews its full body in the shared Modal
502
+ * (focus trap, Esc, scroll lock come from Base UI Dialog - the kanban card
503
+ * pattern); close returns to the grid. `match` is optional - the default is
504
+ * every synced artifact except the task file.
505
+ *
506
+ * Monday-start (ISO), mono day numbers top-right, today inverted. Chips carry
507
+ * the product's basename (max 2 per day + a "+N" overflow); under ~620px of
508
+ * CONTAINER width they collapse to plain dots (measured, not a viewport media
509
+ * query - the dashboard box can be narrow on a wide screen).
510
+ */
511
+ var DOW = [
512
+ "Mon",
513
+ "Tue",
514
+ "Wed",
515
+ "Thu",
516
+ "Fri",
517
+ "Sat",
518
+ "Sun"
519
+ ];
520
+ var DOT_MODE_BELOW = 620;
521
+ var CHIPS_PER_DAY = 2;
522
+ var monthOf = (day) => day.slice(0, 7);
523
+ var basename$1 = (path) => path.split("/").pop() || path;
524
+ /** Human label for how a product got its day (front matter is authoritative). */
525
+ function dateSourceLabel(source) {
526
+ if (source === "frontmatter") return "dated by front matter";
527
+ if (source === "filename") return "dated by filename";
528
+ return "dated by sync time (no date in front matter/filename)";
529
+ }
530
+ var monthIndex = (ym) => {
531
+ const [y, m] = ym.split("-").map(Number);
532
+ return y * 12 + m;
533
+ };
534
+ /**
535
+ * The shown month clamped back into the data: an exact hit stays; a month
536
+ * whose products all vanished (poll refresh) snaps to the nearest remaining
537
+ * one; no pick defaults to the newest month. `months` must be non-empty.
538
+ */
539
+ function clampMonth(months, shown) {
540
+ if (shown && months.includes(shown)) return shown;
541
+ let best = months[months.length - 1];
542
+ if (shown) {
543
+ const target = monthIndex(shown);
544
+ for (const m of months) if (Math.abs(monthIndex(m) - target) < Math.abs(monthIndex(best) - target)) best = m;
545
+ }
546
+ return best;
547
+ }
548
+ var monthLabel = (ym) => {
549
+ const [y, m] = ym.split("-").map(Number);
550
+ return `${new Date(Date.UTC(y, m - 1, 1)).toLocaleString("en-US", {
551
+ month: "long",
552
+ timeZone: "UTC"
553
+ })} ${y}`;
554
+ };
555
+ /** Chip text: basename without extension or a trailing date (digest-2026-07-01.md → "digest"). */
556
+ function chipName(path) {
557
+ const noExt = (path.split("/").pop() || path).replace(/\.[a-z0-9]+$/i, "");
558
+ return noExt.replace(/[-_]?\d{4}([-_]?)\d{2}\1\d{2}$/, "") || noExt;
559
+ }
560
+ /** Measured width of a container element (0 until first measure). */
561
+ function useContainerWidth(ref) {
562
+ const [width, setWidth] = (0, import_react.useState)(0);
563
+ (0, import_react.useEffect)(() => {
564
+ const el = ref.current;
565
+ if (!el || typeof ResizeObserver === "undefined") return;
566
+ const ro = new ResizeObserver((entries) => {
567
+ const w = entries[0]?.contentRect.width;
568
+ if (w != null) setWidth(w);
569
+ });
570
+ ro.observe(el);
571
+ return () => ro.disconnect();
572
+ }, [ref]);
573
+ return width;
574
+ }
575
+ function LoopCalendar({ loopId, artifacts, match, taskFile }) {
576
+ const rootRef = (0, import_react.useRef)(null);
577
+ const width = useContainerWidth(rootRef);
578
+ const dotMode = width > 0 && width < DOT_MODE_BELOW;
579
+ const products = (0, import_react.useMemo)(() => {
580
+ const files = matchArtifacts(artifacts ?? [], match);
581
+ return (match ? files : files.filter((f) => !isTaskPath(taskFile, f.path))).map((f) => ({
582
+ file: f,
583
+ ...productDate(f)
584
+ }));
585
+ }, [
586
+ artifacts,
587
+ match,
588
+ taskFile
589
+ ]);
590
+ const byDay = (0, import_react.useMemo)(() => {
591
+ const m = /* @__PURE__ */ new Map();
592
+ for (const p of products) {
593
+ const list = m.get(p.date) ?? [];
594
+ list.push(p);
595
+ m.set(p.date, list);
596
+ }
597
+ for (const list of m.values()) list.sort((a, b) => a.file.path < b.file.path ? -1 : 1);
598
+ return m;
599
+ }, [products]);
600
+ const months = (0, import_react.useMemo)(() => [...new Set(products.map((p) => monthOf(p.date)))].sort(), [products]);
601
+ const [shown, setShown] = (0, import_react.useState)(null);
602
+ const [reviewing, setReviewing] = (0, import_react.useState)(null);
603
+ const activeReview = reviewing && products.some((p) => p.file.path === reviewing.file.path) ? reviewing : null;
604
+ if (artifacts == null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
605
+ ref: rootRef,
606
+ className: "min-w-0",
607
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
608
+ className: "py-4 text-label text-secondary",
609
+ children: "Loading…"
610
+ })
611
+ });
612
+ if (!products.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
613
+ ref: rootRef,
614
+ className: "min-w-0",
615
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
616
+ className: "rounded-card border border-hairline px-5 py-8 text-center text-body text-disabled",
617
+ children: "No products yet. Files the loop writes appear here after its next run syncs them."
618
+ })
619
+ });
620
+ const month = clampMonth(months, shown);
621
+ const mi = months.indexOf(month);
622
+ const today = localDay((/* @__PURE__ */ new Date()).toISOString());
623
+ const [y, m] = month.split("-").map(Number);
624
+ const lead = (new Date(y, m - 1, 1).getDay() + 6) % 7;
625
+ const dayCount = new Date(y, m, 0).getDate();
626
+ const trail = (7 - (lead + dayCount) % 7) % 7;
627
+ const monthCount = products.filter((p) => monthOf(p.date) === month).length;
628
+ 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";
629
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
630
+ ref: rootRef,
631
+ className: "min-w-0",
632
+ children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
634
+ className: "mb-3 flex flex-wrap items-center gap-x-3 gap-y-2",
635
+ children: [
636
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
637
+ className: "flex items-center gap-1.5",
638
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
639
+ type: "button",
640
+ className: navBtn,
641
+ disabled: mi <= 0,
642
+ onClick: () => setShown(months[mi - 1]),
643
+ "aria-label": "previous month",
644
+ children: "‹"
645
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
646
+ type: "button",
647
+ className: navBtn,
648
+ disabled: mi >= months.length - 1,
649
+ onClick: () => setShown(months[mi + 1]),
650
+ "aria-label": "next month",
651
+ children: "›"
652
+ })]
653
+ }),
654
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
655
+ className: "text-body font-semibold tracking-tight text-display",
656
+ children: monthLabel(month)
657
+ }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
659
+ className: "ml-auto rounded-full bg-raised px-2.5 py-0.5 text-caption font-medium text-secondary",
660
+ children: [
661
+ monthCount,
662
+ " product",
663
+ monthCount === 1 ? "" : "s"
664
+ ]
665
+ })
666
+ ]
667
+ }),
668
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
669
+ className: "grid min-w-0 grid-cols-7 overflow-hidden rounded-card border-l border-t border-hairline",
670
+ children: [
671
+ DOW.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
672
+ 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",
673
+ children: d
674
+ }, d)),
675
+ Array.from({ length: lead }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-r border-hairline bg-raised/20" }, `lead${i}`)),
676
+ Array.from({ length: dayCount }, (_, i) => {
677
+ const day = `${month}-${String(i + 1).padStart(2, "0")}`;
678
+ const fs = byDay.get(day) ?? [];
679
+ const isToday = day === today;
680
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
681
+ 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"}`,
682
+ children: [
683
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
684
+ 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"}`,
685
+ children: i + 1
686
+ }),
687
+ fs.slice(0, CHIPS_PER_DAY).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProductChip, {
688
+ product: p,
689
+ dot: dotMode,
690
+ onPick: () => setReviewing(p)
691
+ }, p.file.path)),
692
+ fs.length > CHIPS_PER_DAY && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
693
+ type: "button",
694
+ onClick: () => setReviewing(fs[CHIPS_PER_DAY]),
695
+ 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",
696
+ children: [
697
+ "+",
698
+ fs.length - CHIPS_PER_DAY,
699
+ " more"
700
+ ]
701
+ })
702
+ ]
703
+ }, day);
704
+ }),
705
+ Array.from({ length: trail }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-r border-hairline bg-raised/20" }, `trail${i}`))
706
+ ]
707
+ }),
708
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
709
+ className: "mt-3 flex flex-wrap gap-x-4 gap-y-1.5",
710
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
711
+ className: "inline-flex items-center gap-1.5 text-caption font-medium text-secondary",
712
+ 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"]
713
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
714
+ className: "inline-flex items-center gap-1.5 text-caption font-medium text-secondary",
715
+ 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"]
716
+ })]
717
+ }),
718
+ activeReview && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
719
+ open: true,
720
+ onClose: () => setReviewing(null),
721
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
722
+ title: activeReview.file.meta?.title?.trim() || basename$1(activeReview.file.path),
723
+ sub: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
724
+ className: "font-mono",
725
+ children: [
726
+ activeReview.file.path,
727
+ " · ",
728
+ dateSourceLabel(activeReview.source),
729
+ " · synced",
730
+ " ",
731
+ fmt(activeReview.file.updatedAt)
732
+ ]
733
+ })
734
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
735
+ className: "mt-4 min-w-0",
736
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArtifactBody, {
737
+ loopId,
738
+ file: activeReview.file
739
+ })
740
+ })]
741
+ })
742
+ ]
743
+ });
744
+ }
745
+ /** One product on a day cell: a named chip, or a plain dot in narrow containers. */
746
+ function ProductChip({ product, dot, onPick }) {
747
+ const fallback = product.source === "sync";
748
+ const title = product.file.path + (fallback ? " · dated by sync time" : "");
749
+ if (dot) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
750
+ type: "button",
751
+ onClick: onPick,
752
+ title,
753
+ "aria-label": product.file.path,
754
+ 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"}`
755
+ });
756
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
757
+ type: "button",
758
+ onClick: onPick,
759
+ title,
760
+ 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" : ""}`,
761
+ children: chipName(product.file.path)
762
+ });
763
+ }
764
+ /**
765
+ * `<loop-kanban columns="research,in-progress,done" match="notes/*.md">` - a
766
+ * COLLECTION VIEW over the loop's front-matter-typed markdown products, rendered
767
+ * as a board. There is NO board file: each artifact IS a card, grouped into the
768
+ * column whose name equals the artifact's front-matter `type`. A card "moves"
769
+ * when a run edits that artifact's `type` in place (the per-run diff shows the
770
+ * move).
771
+ *
772
+ * - `columns` (REQUIRED): comma-separated, explicit order = flow direction, one
773
+ * board column each.
774
+ * - Overflow: any typed artifact whose `type` matches no declared column lands
775
+ * in an automatic trailing "Other" column, so a typo stays visible and a card
776
+ * is never silently dropped. The overflow column is omitted when empty.
777
+ * - `match` (optional glob, same convention as loop-embed/loop-calendar): scopes
778
+ * which artifacts participate; default = every markdown product with a `type`.
779
+ * Untyped artifacts never appear, and the task file is always excluded (its
780
+ * status is the TASK chip, not a kanban type).
781
+ * - Cards: minimal - title (fallback filename) + a small date when the product
782
+ * is actually dated. Sorted date desc, then sync-time desc.
783
+ * - Click a card to review its full body in the shared Modal (focus trap, Esc,
784
+ * scroll lock come from Base UI Dialog); close returns to the board.
785
+ *
786
+ * Layout: the board row scrolls INSIDE its own pane (`min-w-0 overflow-x-auto`),
787
+ * columns are `shrink-0` fixed-width - a wide board never widens the dashboard
788
+ * box or forces a page-level horizontal scrollbar (the Timeline strip rule).
789
+ */
790
+ var OVERFLOW_LABEL = "Other";
791
+ /** Bucket key for the automatic overflow column. A unique Symbol (not the display
792
+ * string) so it can never collide with an author-declared column named 'Other'. */
793
+ var OVERFLOW_KEY = Symbol("kanban-overflow");
794
+ /** React key for the overflow row. Column names are comma-split, so a name can
795
+ * never contain a comma - this sentinel cannot collide with a declared column. */
796
+ var OVERFLOW_REACT_KEY = "overflow,";
797
+ var basename = (path) => path.split("/").pop() || path;
798
+ /** Sort key: product date desc, then sync time desc (newest work first). Both
799
+ * parts are lexically ordered (YYYY-MM-DD / ISO), so a plain string compare on
800
+ * the descending key ranks them. */
801
+ var sortKey = (c) => `${c.date.date}|${c.file.updatedAt}`;
802
+ function LoopKanban({ loopId, artifacts, columns, match, taskFile }) {
803
+ const [reviewing, setReviewing] = (0, import_react.useState)(null);
804
+ const cols = [...new Set((columns ?? "").split(",").map((s) => s.trim()).filter(Boolean))];
805
+ const shell = "min-w-0";
806
+ if (!cols.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
807
+ className: `${shell} rounded-card border border-hairline bg-surface shadow-card`,
808
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
809
+ className: "px-5 py-5 font-mono text-label text-disabled",
810
+ children: "<loop-kanban> needs columns=\"<a,b,c>\""
811
+ })
812
+ });
813
+ if (artifacts == null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
814
+ className: shell,
815
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
816
+ className: "py-4 text-label text-secondary",
817
+ children: "Loading…"
818
+ })
819
+ });
820
+ const cards = matchArtifacts(artifacts, match).filter((a) => isMarkdown(a.path) && !!a.meta?.type && !isTaskPath(taskFile, a.path)).map((file) => ({
821
+ file,
822
+ title: file.meta?.title?.trim() || basename(file.path),
823
+ date: productDate(file)
824
+ }));
825
+ const declared = new Set(cols);
826
+ const grouped = /* @__PURE__ */ new Map();
827
+ for (const card of cards) {
828
+ const type = card.file.meta.type.trim();
829
+ const key = declared.has(type) ? type : OVERFLOW_KEY;
830
+ const list = grouped.get(key) ?? [];
831
+ list.push(card);
832
+ grouped.set(key, list);
833
+ }
834
+ for (const list of grouped.values()) list.sort((a, b) => sortKey(a) < sortKey(b) ? 1 : -1);
835
+ const board = cols.map((name) => ({
836
+ name,
837
+ cards: grouped.get(name) ?? [],
838
+ overflow: false
839
+ }));
840
+ const overflow = grouped.get(OVERFLOW_KEY) ?? [];
841
+ if (overflow.length) board.push({
842
+ name: OVERFLOW_LABEL,
843
+ cards: overflow,
844
+ overflow: true
845
+ });
846
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
847
+ className: `${shell} flex max-h-[420px] gap-3 overflow-x-auto pb-1`,
848
+ children: [board.map((col) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
849
+ className: "flex min-h-0 w-[248px] shrink-0 flex-col",
850
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
851
+ className: "mb-2 flex shrink-0 items-center gap-2 border-b border-hairline pb-1.5",
852
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
853
+ className: `text-label font-semibold ${col.overflow ? "text-disabled" : "text-primary"}`,
854
+ children: col.name
855
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
856
+ className: "text-caption text-disabled",
857
+ children: col.cards.length
858
+ })]
859
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ColumnCards, {
860
+ count: col.cards.length,
861
+ children: col.cards.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
862
+ className: "shrink-0 rounded-control border border-dashed border-hairline px-2.5 py-3 text-center text-caption text-disabled",
863
+ children: "Empty"
864
+ }) : col.cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KanbanCard, {
865
+ card,
866
+ onOpen: () => setReviewing(card)
867
+ }, card.file.path))
868
+ })]
869
+ }, col.overflow ? OVERFLOW_REACT_KEY : col.name)), reviewing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
870
+ open: true,
871
+ onClose: () => setReviewing(null),
872
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
873
+ title: reviewing.title,
874
+ sub: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
875
+ className: "font-mono",
876
+ children: [
877
+ reviewing.file.path,
878
+ reviewing.date.source !== "sync" && ` · ${reviewing.date.date}`,
879
+ " · synced",
880
+ " ",
881
+ fmt(reviewing.file.updatedAt)
882
+ ]
883
+ })
884
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
885
+ className: "mt-4 min-w-0",
886
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArtifactBody, {
887
+ loopId,
888
+ file: reviewing.file
889
+ })
890
+ })]
891
+ })]
892
+ });
893
+ }
894
+ /**
895
+ * A column's scrollable card list. Cards are shrink-0 (they must overflow the
896
+ * height-capped board, never compress to fit it); when there IS overflow below
897
+ * the fold, a bottom fade + "scroll" pill signals it, disappearing at the end.
898
+ */
899
+ function ColumnCards({ count, children }) {
900
+ const ref = (0, import_react.useRef)(null);
901
+ const [below, setBelow] = (0, import_react.useState)(false);
902
+ const update = () => {
903
+ const el = ref.current;
904
+ if (el) setBelow(el.scrollTop + el.clientHeight < el.scrollHeight - 4);
905
+ };
906
+ (0, import_react.useEffect)(update, [count]);
907
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
908
+ className: "relative flex min-h-0 flex-col",
909
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
910
+ ref,
911
+ onScroll: update,
912
+ className: "flex min-h-0 flex-col gap-2 overflow-y-auto pb-1 pr-0.5",
913
+ children
914
+ }), below && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
915
+ 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",
916
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
917
+ className: "mb-1 rounded-full border border-hairline bg-surface px-2 py-0.5 text-micro text-secondary shadow-card",
918
+ children: "↓ scroll"
919
+ })
920
+ })]
921
+ });
922
+ }
923
+ /** One artifact as a card: title + small date, click to review it in the modal. */
924
+ function KanbanCard({ card, onOpen }) {
925
+ const dated = card.date.source !== "sync";
926
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
927
+ className: "min-w-0 shrink-0 overflow-hidden rounded-control border border-hairline bg-surface shadow-card transition-colors hover:border-wire",
928
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
929
+ type: "button",
930
+ onClick: onOpen,
931
+ 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",
932
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
933
+ className: "w-full min-w-0 truncate text-meta text-primary",
934
+ title: card.file.path,
935
+ children: card.title
936
+ }), dated && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
937
+ className: "font-mono text-micro text-disabled",
938
+ children: card.date.date
939
+ })]
940
+ })
941
+ });
942
+ }
943
+ /**
944
+ * Renders a loop's generative-UI template (agent-authored HTML on `Job.ui`).
945
+ *
946
+ * Pipeline: interpolate `{{ ... }}` scalar bindings with live run data → DOMPurify
947
+ * sanitize (allowlisted HTML subset; NO script/handlers/raw-svg) → parse to React,
948
+ * swapping the irreducible data primitives for their renderers. Everything
949
+ * else (A/B panels, stat tiles, layout, text) is the agent's own HTML — there are
950
+ * NO opinionated panel components.
951
+ *
952
+ * <loop-chart series="mrr:MRR:$, paid:Paid"></loop-chart> multi-series trend chart
953
+ * <loop-embed match="reports/digest-*.md"></loop-embed> newest matching artifact, embedded
954
+ * <loop-calendar match="reports/*.md"></loop-calendar> month calendar of produced files
955
+ * <loop-kanban columns="a,b,c" match="notes/*.md"> typed products as a board, columns = type
956
+ *
957
+ * Registering a new primitive means moving three things together: LOOP_TAGS +
958
+ * the sanitizer config below, the parser swap in `options`, and the skill's
959
+ * authoring docs (references/evolve.md §3, run/edit.md) - the sanitizer
960
+ * allowlist and the skill prose must never drift apart.
961
+ */
962
+ var LOOP_TAGS = [
963
+ "loop-chart",
964
+ "loop-embed",
965
+ "loop-calendar",
966
+ "loop-kanban"
967
+ ];
968
+ /** Data-bearing attributes on the loop-* primitives (all parsed by us, never markup). */
969
+ var LOOP_ATTRS = [
970
+ "series",
971
+ "file",
972
+ "match",
973
+ "full",
974
+ "columns"
975
+ ];
976
+ var ARTIFACT_RETRY_MAX = 3;
977
+ var ARTIFACT_RETRY_MS = 4e3;
978
+ var SANITIZE_CONFIG = {
979
+ ALLOWED_TAGS: [
980
+ "h1",
981
+ "h2",
982
+ "h3",
983
+ "h4",
984
+ "h5",
985
+ "p",
986
+ "b",
987
+ "strong",
988
+ "i",
989
+ "em",
990
+ "u",
991
+ "s",
992
+ "span",
993
+ "div",
994
+ "ul",
995
+ "ol",
996
+ "li",
997
+ "table",
998
+ "thead",
999
+ "tbody",
1000
+ "tr",
1001
+ "td",
1002
+ "th",
1003
+ "code",
1004
+ "pre",
1005
+ "br",
1006
+ "hr",
1007
+ "small",
1008
+ "section",
1009
+ "header",
1010
+ "footer",
1011
+ "a",
1012
+ "figure",
1013
+ "figcaption",
1014
+ "mark",
1015
+ ...LOOP_TAGS
1016
+ ],
1017
+ ALLOWED_ATTR: [
1018
+ "style",
1019
+ "class",
1020
+ "href",
1021
+ "title",
1022
+ "target",
1023
+ "rel",
1024
+ ...LOOP_ATTRS
1025
+ ],
1026
+ ADD_TAGS: LOOP_TAGS,
1027
+ CUSTOM_ELEMENT_HANDLING: {
1028
+ tagNameCheck: new RegExp(`^(?:${LOOP_TAGS.join("|")})$`),
1029
+ attributeNameCheck: new RegExp(`^(?:${LOOP_ATTRS.join("|")})$`),
1030
+ allowCustomizedBuiltInElements: false
1031
+ }
1032
+ };
1033
+ purify.addHook("uponSanitizeAttribute", (node, data) => {
1034
+ const tag = node.nodeName?.toLowerCase();
1035
+ if (tag && LOOP_TAGS.includes(tag) && LOOP_ATTRS.includes(data.attrName)) data.forceKeepAttr = true;
1036
+ });
1037
+ function LoopView({ html, runs, loopId, taskFile }) {
1038
+ const clean = (0, import_react.useMemo)(() => {
1039
+ const ctx = buildBindingContext(runs);
1040
+ return purify.sanitize(resolveBindings(html, ctx), SANITIZE_CONFIG);
1041
+ }, [html, runs]);
1042
+ const data = (0, import_react.useMemo)(() => numericSeries(runs), [runs]);
1043
+ const wantsArtifacts = /<loop-(embed|calendar|kanban)\b/.test(clean);
1044
+ const [artifacts, setArtifacts] = (0, import_react.useState)(null);
1045
+ const newestRunId = runs[0]?.id;
1046
+ (0, import_react.useEffect)(() => {
1047
+ if (!wantsArtifacts) return;
1048
+ let alive = true;
1049
+ let retries = 0;
1050
+ let timer;
1051
+ const load = () => {
1052
+ getArtifacts({ data: { loopId } }).then((list) => alive && setArtifacts(list)).catch(() => {
1053
+ if (alive && retries < ARTIFACT_RETRY_MAX) timer = setTimeout(load, ARTIFACT_RETRY_MS * ++retries);
1054
+ });
1055
+ };
1056
+ load();
1057
+ return () => {
1058
+ alive = false;
1059
+ clearTimeout(timer);
1060
+ };
1061
+ }, [
1062
+ wantsArtifacts,
1063
+ loopId,
1064
+ newestRunId,
1065
+ runs[0]?.running === true
1066
+ ]);
1067
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1068
+ className: "loopview text-[14px] leading-relaxed",
1069
+ children: esm_default(clean, (0, import_react.useMemo)(() => ({ replace: (node) => {
1070
+ if (!(node instanceof import_lib.Element)) return void 0;
1071
+ const a = node.attribs ?? {};
1072
+ if (node.name === "loop-chart") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopChart, {
1073
+ data,
1074
+ series: parseSeries(a.series)
1075
+ });
1076
+ if (node.name === "loop-embed") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopEmbed, {
1077
+ loopId,
1078
+ artifacts,
1079
+ file: a.file,
1080
+ match: a.match,
1081
+ full: "full" in a,
1082
+ taskFile
1083
+ });
1084
+ if (node.name === "loop-calendar") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopCalendar, {
1085
+ loopId,
1086
+ artifacts,
1087
+ match: a.match,
1088
+ taskFile
1089
+ });
1090
+ if (node.name === "loop-kanban") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopKanban, {
1091
+ loopId,
1092
+ artifacts,
1093
+ columns: a.columns,
1094
+ match: a.match,
1095
+ taskFile
1096
+ });
1097
+ } }), [
1098
+ data,
1099
+ loopId,
1100
+ taskFile,
1101
+ artifacts
1102
+ ]))
1103
+ });
1104
+ }
1105
+ //#endregion
1106
+ export { LoopView };