@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.
- package/.output/nitro.json +17 -0
- package/.output/public/assets/CodeField-dOzyFQNH.js +34 -0
- package/.output/public/assets/LoopView-B4H9QibC.js +55 -0
- package/.output/public/assets/Modal-D33gNSgx.js +1 -0
- package/.output/public/assets/SignIn-SHuRRyOv.js +1 -0
- package/.output/public/assets/actionUi-ChGGALzg.js +1 -0
- package/.output/public/assets/app-DElSo_jy.css +2 -0
- package/.output/public/assets/artifactView-DFuubXLJ.js +66 -0
- package/.output/public/assets/index-DfpTWRSz.js +12 -0
- package/.output/public/assets/invite._token-DJtd-XE2.js +1 -0
- package/.output/public/assets/invite._token-T8oo4Qtw.js +1 -0
- package/.output/public/assets/jsx-runtime-BseJUIpC.js +1 -0
- package/.output/public/assets/lifecycleUi-DKkChdnU.js +1 -0
- package/.output/public/assets/loopApi-BM2nj71x.js +3 -0
- package/.output/public/assets/loops._loopId-B19Jw7jC.js +5 -0
- package/.output/public/assets/loops._loopId_.runs._runId-_CflHV38.js +3 -0
- package/.output/public/assets/notifyFns-DCubjpRy.js +1 -0
- package/.output/public/assets/preload-helper-CKUiEq3b.js +3 -0
- package/.output/public/assets/routes-B9tgli5f.js +1 -0
- package/.output/public/assets/routes-CTtlM_qt.js +1 -0
- package/.output/public/assets/t._teamId-Bm21mngW.js +1 -0
- package/.output/public/assets/t._teamId-gR7BVKkR.js +1 -0
- package/.output/public/assets/ui-Dp88qUWh.js +1 -0
- package/.output/public/assets/useRouter-DsjH8G0p.js +1 -0
- package/.output/public/favicon.svg +12 -0
- package/.output/public/fonts/GeistPixel-Square.woff2 +0 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +419 -0
- package/.output/server/_libs/@aws-sdk/checksums+[...].mjs +10732 -0
- package/.output/server/_libs/@aws-sdk/client-s3+[...].mjs +9475 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +137 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +440 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +62 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +818 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +59 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +11125 -0
- package/.output/server/_libs/@codemirror/autocomplete+[...].mjs +19713 -0
- package/.output/server/_libs/@codemirror/lang-css+[...].mjs +2636 -0
- package/.output/server/_libs/@codemirror/lang-html+[...].mjs +2247 -0
- package/.output/server/_libs/@codemirror/lang-json+[...].mjs +79 -0
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1736 -0
- package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +342 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +14300 -0
- package/.output/server/_libs/@uiw/codemirror-extensions-basic-setup+[...].mjs +76 -0
- package/.output/server/_libs/atomic-sleep.mjs +27 -0
- package/.output/server/_libs/aws-crypto__crc32c.mjs +314 -0
- package/.output/server/_libs/aws-sdk__nested-clients.mjs +2671 -0
- package/.output/server/_libs/babel__runtime.mjs +30 -0
- package/.output/server/_libs/clsx.mjs +16 -0
- package/.output/server/_libs/codemirror__commands.mjs +1725 -0
- package/.output/server/_libs/codemirror__lint.mjs +565 -0
- package/.output/server/_libs/codemirror__search.mjs +1138 -0
- package/.output/server/_libs/codemirror__theme-one-dark.mjs +161 -0
- package/.output/server/_libs/colorette.mjs +129 -0
- package/.output/server/_libs/croner.mjs +735 -0
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +163 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +689 -0
- package/.output/server/_libs/dateformat.mjs +341 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/diff.mjs +732 -0
- package/.output/server/_libs/dom-serializer+[...].mjs +708 -0
- package/.output/server/_libs/domhandler.mjs +461 -0
- package/.output/server/_libs/dompurify.mjs +2111 -0
- package/.output/server/_libs/domutils.mjs +868 -0
- package/.output/server/_libs/drizzle-orm+postgres.mjs +7689 -0
- package/.output/server/_libs/electric-sql__pglite.mjs +14674 -0
- package/.output/server/_libs/end-of-stream+once+wrappy.mjs +145 -0
- package/.output/server/_libs/es-toolkit.mjs +1737 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/fast-copy.mjs +285 -0
- package/.output/server/_libs/fast-safe-stringify.mjs +172 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +1201 -0
- package/.output/server/_libs/html-dom-parser+htmlparser2.mjs +1560 -0
- package/.output/server/_libs/html-react-parser+[...].mjs +1360 -0
- package/.output/server/_libs/initdb.wasm +0 -0
- package/.output/server/_libs/marked.mjs +1548 -0
- package/.output/server/_libs/on-exit-leak-free.mjs +80 -0
- package/.output/server/_libs/pglite.data +37667 -8
- package/.output/server/_libs/pglite.wasm +0 -0
- package/.output/server/_libs/pino+[...].mjs +3196 -0
- package/.output/server/_libs/pino-abstract-transport+split2.mjs +194 -0
- package/.output/server/_libs/pino-pretty+[...].mjs +1623 -0
- package/.output/server/_libs/pinojs__redact.mjs +280 -0
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+victory-vendor.mjs +15875 -0
- package/.output/server/_libs/tanstack__history.mjs +342 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_libs/uiw__codemirror-themes.mjs +32 -0
- package/.output/server/_libs/uiw__react-codemirror.mjs +354 -0
- package/.output/server/_runtime.mjs +37 -0
- package/.output/server/_ssr/CodeField-tm4rr6G3.mjs +143 -0
- package/.output/server/_ssr/DashboardView-CC7htv28.mjs +2795 -0
- package/.output/server/_ssr/LoopPlaybook-BXH8mhiX.mjs +2105 -0
- package/.output/server/_ssr/LoopView-DCugEbHx.mjs +1106 -0
- package/.output/server/_ssr/Modal-DpKZHLZK.mjs +49 -0
- package/.output/server/_ssr/SignIn-dTndU6Hw.mjs +67 -0
- package/.output/server/_ssr/actionUi-CZbEixbf.mjs +119 -0
- package/.output/server/_ssr/adapters-BS8XosjF.mjs +130 -0
- package/.output/server/_ssr/artifactFiles-Cagtj7J0.mjs +61 -0
- package/.output/server/_ssr/artifactKind-BW0isV8L.mjs +37 -0
- package/.output/server/_ssr/artifactView-DS4O22wl.mjs +304 -0
- package/.output/server/_ssr/artifacts-DngKyW2u.mjs +2 -0
- package/.output/server/_ssr/auth-DY1DuYZd.mjs +38429 -0
- package/.output/server/_ssr/boot-BbtG0ImW.mjs +9010 -0
- package/.output/server/_ssr/boot-CMHTxQpJ.mjs +2 -0
- package/.output/server/_ssr/bun-sqlite-dialect-BW9W1_Ps-CU1Spd6z.mjs +162 -0
- package/.output/server/_ssr/core-RrmWoKne.mjs +5 -0
- package/.output/server/_ssr/createServerRpc-BD8Xn_0_.mjs +12 -0
- package/.output/server/_ssr/d1-sqlite-dialect-BLC8LXE6-DfG7tB8s.mjs +112 -0
- package/.output/server/_ssr/db-DWAuZq1o.mjs +2 -0
- package/.output/server/_ssr/dist-BIxFFVZg.mjs +342 -0
- package/.output/server/_ssr/empty-plugin-adapters-DKUlnwwl.mjs +5 -0
- package/.output/server/_ssr/factory-Blq0DYfT.mjs +6903 -0
- package/.output/server/_ssr/invite._token-BRCo9LGB.mjs +36 -0
- package/.output/server/_ssr/invite._token-DJxGLILJ.mjs +28 -0
- package/.output/server/_ssr/invite._token-NewFt27g.mjs +92 -0
- package/.output/server/_ssr/kysely-adapter-D3XTe64J.mjs +2 -0
- package/.output/server/_ssr/kysely-migration-tables-JkVUjPF_-bPAX9LR_.mjs +19 -0
- package/.output/server/_ssr/lifecycleUi-C9lg1r3r.mjs +38 -0
- package/.output/server/_ssr/logger-Dw82jNjH.mjs +2 -0
- package/.output/server/_ssr/loopApi-BtfkZQYK.mjs +532 -0
- package/.output/server/_ssr/loopApi-Cxe8WyOB.mjs +105 -0
- package/.output/server/_ssr/loops._loopId-7NQJPB9W.mjs +30 -0
- package/.output/server/_ssr/loops._loopId-DhnArGdL.mjs +1635 -0
- package/.output/server/_ssr/loops._loopId_.runs._runId-Bvuo2nki.mjs +29 -0
- package/.output/server/_ssr/loops._loopId_.runs._runId-Dip5Xk-q.mjs +674 -0
- package/.output/server/_ssr/machineFns-Dvr5ICUy.mjs +266 -0
- package/.output/server/_ssr/node-sqlite-dialect-DhyOpJTX.mjs +162 -0
- package/.output/server/_ssr/notifyFns-BcMRY5kP.mjs +118 -0
- package/.output/server/_ssr/notifyFns-Q458pZmE.mjs +580 -0
- package/.output/server/_ssr/router-J8KZ30xw.mjs +678 -0
- package/.output/server/_ssr/routes-Bcs4T1ox.mjs +28 -0
- package/.output/server/_ssr/routes-BoGrcf-b.mjs +43 -0
- package/.output/server/_ssr/routes-D4am3Zff.mjs +28 -0
- package/.output/server/_ssr/runDiff-CpEFqo7G.mjs +113 -0
- package/.output/server/_ssr/ssr.mjs +5837 -0
- package/.output/server/_ssr/start-4ifArtKx.mjs +4 -0
- package/.output/server/_ssr/store-b3JcQ08F.mjs +2 -0
- package/.output/server/_ssr/t._teamId-CeUhzgzT.mjs +32 -0
- package/.output/server/_ssr/t._teamId-DR-hQK7o.mjs +47 -0
- package/.output/server/_ssr/t._teamId-Dy_McbRt.mjs +31 -0
- package/.output/server/_ssr/teamCookie-B7hLOIie.mjs +10 -0
- package/.output/server/_ssr/teamFns-BIDnsg2A.mjs +473 -0
- package/.output/server/_ssr/teamFns-DA4ovI5g.mjs +33 -0
- package/.output/server/_ssr/tokens-ytd60FUt.mjs +2 -0
- package/.output/server/_ssr/ui-Dej2qjuS.mjs +267 -0
- package/.output/server/_tanstack-start-manifest_v-CVS6c7re.mjs +95 -0
- package/.output/server/drizzle/0000_round_marauders.sql +193 -0
- package/.output/server/drizzle/0001_naive_silver_fox.sql +25 -0
- package/.output/server/drizzle/0002_funny_sage.sql +12 -0
- package/.output/server/drizzle/0003_optimal_slapstick.sql +102 -0
- package/.output/server/drizzle/0004_remove_grok_agent.sql +5 -0
- package/.output/server/drizzle/0005_heavy_gauntlet.sql +8 -0
- package/.output/server/drizzle/0006_tranquil_sharon_carter.sql +1 -0
- package/.output/server/drizzle/0007_faulty_starbolt.sql +30 -0
- package/.output/server/drizzle/0008_durable_lifecycle_evidence.sql +15 -0
- package/.output/server/drizzle/meta/0000_snapshot.json +1333 -0
- package/.output/server/drizzle/meta/0001_snapshot.json +1498 -0
- package/.output/server/drizzle/meta/0002_snapshot.json +1576 -0
- package/.output/server/drizzle/meta/0003_snapshot.json +1666 -0
- package/.output/server/drizzle/meta/0005_snapshot.json +1666 -0
- package/.output/server/drizzle/meta/0006_snapshot.json +1672 -0
- package/.output/server/drizzle/meta/0007_snapshot.json +1787 -0
- package/.output/server/drizzle/meta/_journal.json +69 -0
- package/.output/server/index.mjs +464 -0
- package/.output/server/node_modules/tslib/modules/index.js +70 -0
- package/.output/server/node_modules/tslib/modules/package.json +3 -0
- package/.output/server/node_modules/tslib/package.json +47 -0
- package/.output/server/node_modules/tslib/tslib.js +484 -0
- package/.output/server/package.json +9 -0
- package/LICENSE +21 -0
- package/drizzle/0000_round_marauders.sql +193 -0
- package/drizzle/0001_naive_silver_fox.sql +25 -0
- package/drizzle/0002_funny_sage.sql +12 -0
- package/drizzle/0003_optimal_slapstick.sql +102 -0
- package/drizzle/0004_remove_grok_agent.sql +5 -0
- package/drizzle/0005_heavy_gauntlet.sql +8 -0
- package/drizzle/0006_tranquil_sharon_carter.sql +1 -0
- package/drizzle/0007_faulty_starbolt.sql +30 -0
- package/drizzle/0008_durable_lifecycle_evidence.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +1333 -0
- package/drizzle/meta/0001_snapshot.json +1498 -0
- package/drizzle/meta/0002_snapshot.json +1576 -0
- package/drizzle/meta/0003_snapshot.json +1666 -0
- package/drizzle/meta/0005_snapshot.json +1666 -0
- package/drizzle/meta/0006_snapshot.json +1672 -0
- package/drizzle/meta/0007_snapshot.json +1787 -0
- package/drizzle/meta/_journal.json +69 -0
- package/package.json +84 -0
- package/scripts/pievo-server.mjs +390 -0
- package/scripts/prestart.mjs +66 -0
- 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
|
+
"&": "&",
|
|
41
|
+
"<": "<",
|
|
42
|
+
">": ">",
|
|
43
|
+
"\"": """
|
|
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 };
|