dsh-report-ledger 0.1.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/lib/client.js ADDED
@@ -0,0 +1,939 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-report-ledger",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/timeline-model.ts
10
+ /**
11
+ * Build the merged, time-ordered row list.
12
+ *
13
+ * Sessions are placed at their creation time and reports at theirs, so the list
14
+ * reads as the collaboration actually unfolded: a session appears where it was
15
+ * branched, a report where it was opened. Report depth comes from the session
16
+ * that authored it, which is what keeps the indentation meaningful even when the
17
+ * session row itself is filtered out.
18
+ * @param payload - the endpoint payload.
19
+ * @param basis - whether to place a report at its creation or its last activity.
20
+ * @returns the rows, oldest first.
21
+ */
22
+ function buildRows(payload, basis = "created") {
23
+ const depth = /* @__PURE__ */ new Map();
24
+ const rows = [];
25
+ for (const node of payload.sessions) {
26
+ depth.set(node.id, node.depth);
27
+ rows.push({
28
+ kind: "session",
29
+ at: node.createdAt ?? 0,
30
+ id: node.id,
31
+ depth: node.depth,
32
+ delegated: node.delegated,
33
+ live: node.live,
34
+ ...node.title === void 0 ? {} : { title: node.title }
35
+ });
36
+ }
37
+ for (const front of payload.reports) rows.push({
38
+ kind: "report",
39
+ at: basis === "created" ? front.created : front.updated,
40
+ depth: depth.get(front.from) ?? 0,
41
+ front
42
+ });
43
+ rows.sort((left, right) => left.at - right.at || (left.kind === right.kind ? 0 : left.kind === "session" ? -1 : 1));
44
+ return rows;
45
+ }
46
+ /** Every string a report row can be found by. */
47
+ function reportHaystack(front) {
48
+ return [
49
+ front.report,
50
+ front.subject,
51
+ front.from,
52
+ front.fromName ?? "",
53
+ ...front.to,
54
+ ...front.cc,
55
+ ...front.authors,
56
+ front.task ?? "",
57
+ ...front.artifacts
58
+ ].join("\n").toLowerCase();
59
+ }
60
+ /** Every string a session row can be found by. */
61
+ function sessionHaystack(row) {
62
+ return `${row.title ?? ""}\n${row.id}`.toLowerCase();
63
+ }
64
+ /**
65
+ * Whether one row survives the current filter.
66
+ * @param row - the candidate row.
67
+ * @param filter - the active filter.
68
+ * @returns true when the row should be rendered.
69
+ */
70
+ function rowMatches(row, filter) {
71
+ const needle = filter.text.trim().toLowerCase();
72
+ if (row.kind === "session") return needle === "" || sessionHaystack(row).includes(needle);
73
+ if (filter.status !== "all" && row.front.status !== filter.status) return false;
74
+ return needle === "" || reportHaystack(row.front).includes(needle);
75
+ }
76
+ /**
77
+ * Apply a filter to the row list.
78
+ * @param rows - the assembled rows.
79
+ * @param filter - the active filter.
80
+ * @returns the surviving rows, in their original order.
81
+ */
82
+ function filterRows(rows, filter) {
83
+ return rows.filter((row) => rowMatches(row, filter));
84
+ }
85
+ /**
86
+ * Count reports by lifecycle state.
87
+ *
88
+ * Counted from the whole subtree payload rather than the filtered rows, so the
89
+ * chips keep showing the full picture while one of them is applied — a filter
90
+ * whose own label changed when you used it would make it impossible to see how
91
+ * much you had excluded.
92
+ * @param reports - every report in the payload.
93
+ * @returns the counts.
94
+ */
95
+ function statusCounts(reports) {
96
+ const counts = {
97
+ all: reports.length,
98
+ open: 0,
99
+ acked: 0,
100
+ closed: 0
101
+ };
102
+ for (const front of reports) if (front.status === "open") counts.open += 1;
103
+ else if (front.status === "acked") counts.acked += 1;
104
+ else if (front.status === "closed") counts.closed += 1;
105
+ return counts;
106
+ }
107
+ /**
108
+ * Whether a filter would hide anything.
109
+ * @param filter - the active filter.
110
+ * @returns true when a status or text constraint is in force.
111
+ */
112
+ function isFiltering(filter) {
113
+ return filter.status !== "all" || filter.text.trim() !== "";
114
+ }
115
+ /**
116
+ * Count how many report rows are visible under a filter.
117
+ * @param rows - every assembled row.
118
+ * @param filter - the active filter.
119
+ * @returns the number of visible report rows.
120
+ */
121
+ function visibleReports(rows, filter) {
122
+ let count = 0;
123
+ for (const row of filterRows(rows, filter)) if (row.kind === "report") count += 1;
124
+ return count;
125
+ }
126
+ /**
127
+ * The report ids present in one payload.
128
+ *
129
+ * A thread link can point outside the subtree — a report answering one opened by
130
+ * an unrelated session, for instance — and the detail endpoint is ledger-scoped,
131
+ * so such a report still opens. Knowing which ids are in the list is what lets
132
+ * the view say so instead of silently showing a card that is not in the timeline.
133
+ * @param payload - the endpoint payload.
134
+ * @returns the ids of every report in the payload.
135
+ */
136
+ function reportIds(payload) {
137
+ const ids = /* @__PURE__ */ new Set();
138
+ for (const front of payload.reports) ids.add(front.report);
139
+ return ids;
140
+ }
141
+ /** Body length above which the detail panel shows only a head. */
142
+ const BODY_DISPLAY_LIMIT = 4e3;
143
+ /** How much of an over-long body is shown. */
144
+ const BODY_HEAD_LIMIT = 1200;
145
+ /**
146
+ * Trim a body for the detail panel.
147
+ *
148
+ * Mirrors what `report_read` already does for the model: the full text stays in
149
+ * the ledger file, and both readers get a bounded view plus a pointer rather than
150
+ * an unbounded paste. Keeping the human view and the model view bounded the same
151
+ * way means neither can be surprised by what the other sees.
152
+ * @param body - the raw body.
153
+ * @param limit - length above which truncation applies.
154
+ * @param head - how many characters to keep when truncating.
155
+ * @returns the display text and whether it was cut.
156
+ */
157
+ function displayBody(body, limit = BODY_DISPLAY_LIMIT, head = BODY_HEAD_LIMIT) {
158
+ if (body.length <= limit) return {
159
+ text: body,
160
+ truncated: false
161
+ };
162
+ return {
163
+ text: body.slice(0, head),
164
+ truncated: true
165
+ };
166
+ }
167
+ //#endregion
168
+ //#region src/client/ReportsView.tsx
169
+ /**
170
+ * The report timeline view — the `conversation.view` tab this plugin registers.
171
+ *
172
+ * It renders one top-to-bottom timeline of the collaboration: the session
173
+ * subtree as it branched, interleaved with the reports those sessions produced
174
+ * or were copied on. Rows carry only the digest, so a long-running tree stays
175
+ * scannable; clicking a report opens its transfer path and body on demand.
176
+ *
177
+ * Data comes from this plugin's own read-only endpoint rather than a generated
178
+ * remote: the host assembles the subtree and filters the ledger to the sessions
179
+ * in it, which the browser cannot do on its own because a report's transfer path
180
+ * is a host-side file, not session state.
181
+ *
182
+ * A blank session never reaches this component — the conversation slot is
183
+ * omitted entirely in that state.
184
+ *
185
+ * @module dsh-report-ledger/client/ReportsView
186
+ */
187
+ const TIMELINE_URL = "/api/report-ledger/timeline";
188
+ const REPORT_URL = "/api/report-ledger/report";
189
+ /** Read one endpoint and unwrap its result envelope. */
190
+ async function getJson(url) {
191
+ const response = await fetch(url, {
192
+ credentials: "same-origin",
193
+ headers: { accept: "application/json" }
194
+ });
195
+ let body;
196
+ try {
197
+ body = await response.json();
198
+ } catch {
199
+ throw new Error(`HTTP ${response.status}`);
200
+ }
201
+ const envelope = body;
202
+ if (envelope.ok !== true) throw new Error(envelope.code ?? `HTTP ${response.status}`);
203
+ return envelope.value;
204
+ }
205
+ /** Substitute `{name}` placeholders. */
206
+ function fill(template, params) {
207
+ return template.replace(/\{(\w+)\}/g, (whole, name) => Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : whole);
208
+ }
209
+ /** Format one timestamp for a timeline row. */
210
+ function stamp(at) {
211
+ if (at === void 0 || at <= 0) return "";
212
+ return new Date(at).toLocaleString();
213
+ }
214
+ /** Colour a status chip from the theme's state tokens. */
215
+ function statusStyle(status) {
216
+ if (status === "acked") return {
217
+ color: "var(--dsw-alias-state-success-label, #1a7f37)",
218
+ background: "var(--dsw-alias-state-success-bg, rgba(26,127,55,0.10))"
219
+ };
220
+ if (status === "closed") return {
221
+ color: "var(--dsw-alias-label-tertiary, #888)",
222
+ background: "var(--dsw-alias-bg-layer-2, rgba(0,0,0,0.04))"
223
+ };
224
+ return {
225
+ color: "var(--dsw-alias-state-warn-label, #9a6700)",
226
+ background: "var(--dsw-alias-state-warn-bg, rgba(154,103,0,0.10))"
227
+ };
228
+ }
229
+ /** Storage key for the remembered status filter. */
230
+ const FILTER_KEY = "dsh.reportLedger.filter.v1";
231
+ /** Read the remembered status filter, tolerating absent or damaged storage. */
232
+ function rememberedStatus() {
233
+ try {
234
+ const raw = globalThis.localStorage?.getItem(FILTER_KEY);
235
+ if (typeof raw === "string") {
236
+ const parsed = JSON.parse(raw);
237
+ if (parsed.status === "open" || parsed.status === "acked" || parsed.status === "closed") return parsed.status;
238
+ }
239
+ } catch {}
240
+ return "all";
241
+ }
242
+ /**
243
+ * Render the report timeline.
244
+ * @param props - the session binding and the locale seat.
245
+ * @returns the tab content.
246
+ */
247
+ function ReportsView(props) {
248
+ const { sessionId, t } = props;
249
+ const [payload, setPayload] = (0, react.useState)(void 0);
250
+ const [error, setError] = (0, react.useState)(void 0);
251
+ const [nonce, setNonce] = (0, react.useState)(0);
252
+ const [openReport, setOpenReport] = (0, react.useState)(void 0);
253
+ const [detail, setDetail] = (0, react.useState)(void 0);
254
+ const [detailError, setDetailError] = (0, react.useState)(void 0);
255
+ const [status, setStatus] = (0, react.useState)(rememberedStatus);
256
+ const [text, setText] = (0, react.useState)("");
257
+ const [basis, setBasis] = (0, react.useState)("created");
258
+ (0, react.useEffect)(() => {
259
+ try {
260
+ globalThis.localStorage?.setItem(FILTER_KEY, JSON.stringify({ status }));
261
+ } catch {}
262
+ }, [status]);
263
+ (0, react.useEffect)(() => {
264
+ if (sessionId === void 0 || sessionId === "") {
265
+ setError("no-session");
266
+ return;
267
+ }
268
+ let cancelled = false;
269
+ setError(void 0);
270
+ getJson(`${TIMELINE_URL}?root=${encodeURIComponent(sessionId)}`).then((value) => {
271
+ if (!cancelled) setPayload(value);
272
+ }).catch((cause) => {
273
+ if (!cancelled) setError(cause instanceof Error ? cause.message : String(cause));
274
+ });
275
+ return () => {
276
+ cancelled = true;
277
+ };
278
+ }, [sessionId, nonce]);
279
+ (0, react.useEffect)(() => {
280
+ if (openReport === void 0) return;
281
+ let cancelled = false;
282
+ setDetail(void 0);
283
+ setDetailError(void 0);
284
+ getJson(`${REPORT_URL}?id=${encodeURIComponent(openReport)}`).then((value) => {
285
+ if (!cancelled) setDetail(value);
286
+ }).catch((cause) => {
287
+ if (!cancelled) setDetailError(cause instanceof Error ? cause.message : String(cause));
288
+ });
289
+ return () => {
290
+ cancelled = true;
291
+ };
292
+ }, [openReport, nonce]);
293
+ const rows = (0, react.useMemo)(() => payload === void 0 ? [] : buildRows(payload, basis), [payload, basis]);
294
+ const counts = (0, react.useMemo)(() => statusCounts(payload?.reports ?? []), [payload]);
295
+ const inList = (0, react.useMemo)(() => payload === void 0 ? /* @__PURE__ */ new Set() : reportIds(payload), [payload]);
296
+ const shownBody = (0, react.useMemo)(() => displayBody(detail?.body ?? ""), [detail]);
297
+ const filter = (0, react.useMemo)(() => ({
298
+ status,
299
+ text
300
+ }), [status, text]);
301
+ const visible = (0, react.useMemo)(() => {
302
+ const kept = filterRows(rows, filter);
303
+ if (openReport === void 0 || detail === void 0) return kept;
304
+ if (kept.some((row) => row.kind === "report" && row.front.report === openReport)) return kept;
305
+ return [...kept, {
306
+ kind: "report",
307
+ at: detail.front.created,
308
+ depth: 0,
309
+ front: detail.front
310
+ }];
311
+ }, [
312
+ rows,
313
+ filter,
314
+ openReport,
315
+ detail
316
+ ]);
317
+ const filteredReportIds = (0, react.useMemo)(() => {
318
+ const ids = /* @__PURE__ */ new Set();
319
+ for (const row of filterRows(rows, filter)) if (row.kind === "report") ids.add(row.front.report);
320
+ return ids;
321
+ }, [rows, filter]);
322
+ const filtering = isFiltering(filter);
323
+ const visibleReportCount = (0, react.useMemo)(() => visibleReports(rows, filter), [rows, filter]);
324
+ const refresh = (0, react.useCallback)(() => {
325
+ setNonce((value) => value + 1);
326
+ }, []);
327
+ const shell = {
328
+ padding: "16px 20px",
329
+ overflowY: "auto",
330
+ height: "100%",
331
+ font: "var(--dsw-font-xs-13, 13px/1.6 system-ui, sans-serif)",
332
+ color: "var(--dsw-alias-label-primary, inherit)"
333
+ };
334
+ const head = {
335
+ display: "flex",
336
+ alignItems: "baseline",
337
+ gap: "12px",
338
+ marginBottom: "12px"
339
+ };
340
+ const summary = {
341
+ color: "var(--dsw-alias-label-tertiary, #888)",
342
+ fontSize: "var(--dsw-font-xxs-12, 12)",
343
+ flex: "1 1 auto"
344
+ };
345
+ const button = {
346
+ font: "inherit",
347
+ color: "var(--dsw-alias-label-secondary, inherit)",
348
+ background: "var(--dsw-alias-bg-layer-2, transparent)",
349
+ border: "1px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))",
350
+ borderRadius: "6px",
351
+ padding: "3px 10px",
352
+ cursor: "pointer"
353
+ };
354
+ const chip = (extra) => ({
355
+ display: "inline-block",
356
+ padding: "0 6px",
357
+ borderRadius: "999px",
358
+ fontSize: "var(--dsw-font-xxxs-11, 11)",
359
+ lineHeight: "17px",
360
+ marginRight: "6px",
361
+ ...extra
362
+ });
363
+ const tools = {
364
+ display: "flex",
365
+ alignItems: "center",
366
+ flexWrap: "wrap",
367
+ gap: "6px",
368
+ marginBottom: "10px"
369
+ };
370
+ const time = {
371
+ color: "var(--dsw-alias-label-caption, #999)",
372
+ fontSize: "var(--dsw-font-xxxs-11, 11)",
373
+ whiteSpace: "nowrap"
374
+ };
375
+ if (error !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
376
+ style: shell,
377
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
378
+ style: { color: "var(--dsw-alias-state-error-label, #b00)" },
379
+ children: t("view.error", { code: error })
380
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
381
+ style: { marginTop: "8px" },
382
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
383
+ type: "button",
384
+ style: button,
385
+ onClick: refresh,
386
+ children: t("view.retry")
387
+ })
388
+ })]
389
+ });
390
+ if (payload === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
391
+ style: shell,
392
+ children: t("view.loading")
393
+ });
394
+ const statusLabel = {
395
+ all: "filter.all",
396
+ open: "filter.open",
397
+ acked: "filter.acked",
398
+ closed: "filter.closed"
399
+ };
400
+ const statusCount = {
401
+ all: counts.all,
402
+ open: counts.open,
403
+ acked: counts.acked,
404
+ closed: counts.closed
405
+ };
406
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
407
+ style: shell,
408
+ children: [
409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
410
+ style: head,
411
+ children: [
412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("view.tab") }),
413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
414
+ style: summary,
415
+ children: filtering ? t("filter.showing", {
416
+ visible: visibleReportCount,
417
+ total: counts.all
418
+ }) : t("view.summary", {
419
+ sessions: payload.sessions.length,
420
+ reports: payload.reports.length
421
+ })
422
+ }),
423
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
424
+ type: "button",
425
+ style: button,
426
+ onClick: refresh,
427
+ children: t("view.refresh")
428
+ })
429
+ ]
430
+ }),
431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
432
+ style: tools,
433
+ title: t("filter.hint"),
434
+ children: [
435
+ [
436
+ "all",
437
+ "open",
438
+ "acked",
439
+ "closed"
440
+ ].map((candidate) => {
441
+ const active = status === candidate;
442
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
443
+ type: "button",
444
+ "aria-pressed": active,
445
+ onClick: () => {
446
+ setStatus(candidate);
447
+ },
448
+ style: {
449
+ ...button,
450
+ ...active ? {
451
+ color: "var(--dsw-alias-label-primary, inherit)",
452
+ background: "var(--dsw-alias-interactive-bg-active, rgba(0,0,0,0.08))",
453
+ borderColor: "var(--dsw-alias-border-l3, rgba(0,0,0,0.20))"
454
+ } : {}
455
+ },
456
+ children: `${t(statusLabel[candidate])} ${statusCount[candidate]}`
457
+ }, candidate);
458
+ }),
459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
460
+ type: "search",
461
+ value: text,
462
+ placeholder: t("filter.search"),
463
+ "aria-label": t("filter.search"),
464
+ onChange: (event) => {
465
+ setText(event.target.value);
466
+ },
467
+ style: {
468
+ flex: "1 1 200px",
469
+ minWidth: "140px",
470
+ padding: "3px 8px",
471
+ font: "inherit",
472
+ color: "var(--dsw-alias-label-primary, inherit)",
473
+ background: "var(--dsw-alias-bg-base, rgba(0,0,0,0.02))",
474
+ border: "1px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))",
475
+ borderRadius: "6px"
476
+ }
477
+ }),
478
+ filtering ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
479
+ type: "button",
480
+ style: button,
481
+ onClick: () => {
482
+ setStatus("all");
483
+ setText("");
484
+ },
485
+ children: t("filter.clear")
486
+ }) : null,
487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
488
+ type: "button",
489
+ style: button,
490
+ title: t("basis.hint"),
491
+ onClick: () => {
492
+ setBasis(basis === "created" ? "updated" : "created");
493
+ },
494
+ children: t(basis === "created" ? "basis.created" : "basis.updated")
495
+ })
496
+ ]
497
+ }),
498
+ rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
499
+ style: { color: "var(--dsw-alias-label-tertiary, #888)" },
500
+ children: t("view.empty")
501
+ }) : null,
502
+ rows.length > 0 && visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
503
+ style: { color: "var(--dsw-alias-label-tertiary, #888)" },
504
+ children: t("filter.none")
505
+ }) : null,
506
+ visible.map((row, index) => {
507
+ const pad = 8 + row.depth * 18;
508
+ if (row.kind === "session") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
509
+ style: {
510
+ display: "flex",
511
+ gap: "8px",
512
+ padding: `4px 0 4px ${pad}px`,
513
+ alignItems: "baseline"
514
+ },
515
+ children: [
516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
517
+ style: { color: "var(--dsw-alias-label-tertiary, #999)" },
518
+ children: "●"
519
+ }),
520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
521
+ style: { flex: "1 1 auto" },
522
+ children: [
523
+ row.delegated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
524
+ style: chip({
525
+ color: "var(--dsw-alias-state-business-label, #0969da)",
526
+ background: "var(--dsw-alias-state-business-bg, rgba(9,105,218,0.10))"
527
+ }),
528
+ children: t("view.delegated")
529
+ }) : null,
530
+ row.live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
531
+ style: chip({
532
+ color: "var(--dsw-alias-state-success-label, #1a7f37)",
533
+ background: "var(--dsw-alias-state-success-bg, rgba(26,127,55,0.10))"
534
+ }),
535
+ children: t("view.live")
536
+ }) : null,
537
+ row.title ?? row.id
538
+ ]
539
+ }),
540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
541
+ style: time,
542
+ children: stamp(row.at)
543
+ })
544
+ ]
545
+ }, `s-${row.id}-${index}`);
546
+ const front = row.front;
547
+ const expanded = openReport === front.report;
548
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
549
+ style: { paddingLeft: `${pad}px` },
550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
551
+ role: "button",
552
+ tabIndex: 0,
553
+ onClick: () => {
554
+ setOpenReport(expanded ? void 0 : front.report);
555
+ },
556
+ onKeyDown: (event) => {
557
+ if (event.key === "Enter" || event.key === " ") {
558
+ event.preventDefault();
559
+ setOpenReport(expanded ? void 0 : front.report);
560
+ }
561
+ },
562
+ style: {
563
+ display: "flex",
564
+ gap: "8px",
565
+ alignItems: "baseline",
566
+ padding: "5px 8px",
567
+ margin: "2px 0",
568
+ borderRadius: "6px",
569
+ cursor: "pointer",
570
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.02))",
571
+ border: "1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.06))"
572
+ },
573
+ children: [
574
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
575
+ style: { color: "var(--dsw-alias-label-tertiary, #999)" },
576
+ children: expanded ? "▾" : "▸"
577
+ }),
578
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
579
+ style: { flex: "1 1 auto" },
580
+ children: [
581
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
582
+ style: chip(statusStyle(front.status)),
583
+ children: front.status
584
+ }),
585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: front.report }),
586
+ " ",
587
+ front.subject,
588
+ front.task === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
589
+ type: "button",
590
+ title: fill(t("row.filterByTask"), { task: front.task }),
591
+ onClick: (event) => {
592
+ event.stopPropagation();
593
+ setText(front.task);
594
+ },
595
+ style: {
596
+ ...chip({
597
+ color: "var(--dsw-alias-state-business-label, #0969da)",
598
+ background: "var(--dsw-alias-state-business-bg, rgba(9,105,218,0.10))"
599
+ }),
600
+ border: 0,
601
+ cursor: "pointer",
602
+ font: "inherit"
603
+ },
604
+ children: front.task
605
+ }),
606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
607
+ style: {
608
+ ...time,
609
+ marginTop: "2px"
610
+ },
611
+ children: [
612
+ `from=${front.from}`,
613
+ front.to.length > 0 ? ` → ${front.to.join(", ")}` : "",
614
+ front.cc.length > 0 ? ` cc ${front.cc.join(", ")}` : "",
615
+ ` · ${fill(t("row.hops"), { count: front.hops })}`,
616
+ front.authors.length > 1 ? ` · ${fill(t("row.authors"), { count: front.authors.length })}` : "",
617
+ front.last === void 0 ? "" : ` · last=${front.last.action}`
618
+ ]
619
+ })
620
+ ]
621
+ }),
622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
623
+ style: time,
624
+ children: stamp(front.updated)
625
+ })
626
+ ]
627
+ }), expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
628
+ style: {
629
+ margin: "4px 0 10px 24px",
630
+ padding: "10px 12px",
631
+ borderRadius: "6px",
632
+ background: "var(--dsw-alias-bg-layer-2, rgba(0,0,0,0.03))",
633
+ border: "1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.06))"
634
+ },
635
+ children: [
636
+ detailError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
637
+ style: { color: "var(--dsw-alias-state-error-label, #b00)" },
638
+ children: t("view.error", { code: detailError })
639
+ }) : null,
640
+ detail === void 0 && detailError === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
641
+ style: time,
642
+ children: t("view.detail.loading")
643
+ }) : null,
644
+ detail === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
646
+ style: { marginBottom: "6px" },
647
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("view.detail.route") })
648
+ }),
649
+ detail.hops.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
650
+ style: time,
651
+ children: t("view.detail.routeEmpty")
652
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
653
+ style: {
654
+ display: "grid",
655
+ gridTemplateColumns: "auto auto auto 1fr",
656
+ gap: "2px 10px",
657
+ marginBottom: "8px"
658
+ },
659
+ children: detail.hops.map((hop, hopIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
661
+ style: time,
662
+ children: stamp(hop.at)
663
+ }),
664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: hop.action }) }),
665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
666
+ style: time,
667
+ children: hop.actor
668
+ }),
669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
670
+ style: time,
671
+ children: hop.to.length === 0 ? hop.note ?? "" : `→ ${hop.to.join(", ")}${hop.note === void 0 ? "" : ` (${hop.note})`}`
672
+ })
673
+ ] }, `h-${hopIndex}`))
674
+ }),
675
+ detail.pending.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
676
+ style: {
677
+ ...time,
678
+ color: "var(--dsw-alias-state-warn-label, #9a6700)"
679
+ },
680
+ children: fill(t("view.detail.pending"), { targets: detail.pending.join(", ") })
681
+ }),
682
+ (() => {
683
+ const parent = detail.front.parent;
684
+ const children = detail.front.children;
685
+ if (parent === void 0 && children.length === 0) return null;
686
+ const link = (target) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
687
+ type: "button",
688
+ title: fill(t("detail.openLinked"), { report: target }),
689
+ onClick: () => {
690
+ setOpenReport(target);
691
+ },
692
+ style: {
693
+ ...button,
694
+ marginRight: "6px",
695
+ font: "var(--dsw-font-xxxs-11, 11px/1.4 ui-monospace, monospace)"
696
+ },
697
+ children: target
698
+ }, target);
699
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
700
+ style: { marginBottom: "8px" },
701
+ children: [
702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("detail.thread") }),
703
+ parent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
704
+ style: { marginTop: "2px" },
705
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
706
+ style: time,
707
+ children: `${t("detail.parent")}: `
708
+ }), link(parent)]
709
+ }),
710
+ children.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
711
+ style: { marginTop: "2px" },
712
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
713
+ style: time,
714
+ children: `${t("detail.children")}: `
715
+ }), children.map(link)]
716
+ })
717
+ ]
718
+ });
719
+ })(),
720
+ inList.has(detail.front.report) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
721
+ style: {
722
+ ...time,
723
+ marginBottom: "8px"
724
+ },
725
+ children: [
726
+ fill(t("detail.outside"), { report: detail.front.report }),
727
+ " ",
728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
729
+ type: "button",
730
+ style: button,
731
+ onClick: () => {
732
+ setOpenReport(void 0);
733
+ },
734
+ children: t("detail.backToList")
735
+ })
736
+ ]
737
+ }),
738
+ inList.has(detail.front.report) && !filteredReportIds.has(detail.front.report) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
739
+ style: {
740
+ ...time,
741
+ marginBottom: "8px"
742
+ },
743
+ children: fill(t("detail.filteredOut"), { report: detail.front.report })
744
+ }) : null,
745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
746
+ style: { margin: "8px 0 4px" },
747
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("view.detail.body") })
748
+ }),
749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
750
+ style: {
751
+ margin: 0,
752
+ padding: "8px 10px",
753
+ maxHeight: "320px",
754
+ overflow: "auto",
755
+ whiteSpace: "pre-wrap",
756
+ wordBreak: "break-word",
757
+ background: "var(--dsw-alias-bg-base, rgba(0,0,0,0.02))",
758
+ border: "1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.06))",
759
+ borderRadius: "4px",
760
+ font: "var(--dsw-font-xs-13, 13px/1.5 ui-monospace, monospace)"
761
+ },
762
+ children: shownBody.text.trim()
763
+ }),
764
+ shownBody.truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
765
+ style: {
766
+ ...time,
767
+ marginTop: "4px"
768
+ },
769
+ children: fill(t("detail.bodyTruncated"), { chars: shownBody.text.trim().length })
770
+ }) : null,
771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
772
+ style: {
773
+ ...time,
774
+ marginTop: "6px",
775
+ wordBreak: "break-all"
776
+ },
777
+ children: [
778
+ t("view.detail.path"),
779
+ ": ",
780
+ detail.path
781
+ ]
782
+ })
783
+ ] })
784
+ ]
785
+ }) : null]
786
+ }, `r-${front.report}`);
787
+ })
788
+ ]
789
+ });
790
+ }
791
+ //#endregion
792
+ //#region src/client/locales.ts
793
+ /**
794
+ * Dictionary for the report timeline tab.
795
+ *
796
+ * Both shipped locales are required by the client locale service, and the key
797
+ * union drives the typed `t()` the view uses, so a missing translation is a
798
+ * type error rather than an empty label.
799
+ *
800
+ * @module dsh-report-ledger/client/locales
801
+ */
802
+ /** Chinese dictionary — the source of truth for the key set. */
803
+ const zh = {
804
+ "view.tab": "汇报",
805
+ "view.loading": "正在读取账本…",
806
+ "view.summary": "{sessions} 个会话 · {reports} 份汇报",
807
+ "view.empty": "这个会话树下还没有汇报。代理用 report_author 开一份汇报后,这里会出现它的缩略卡。",
808
+ "view.refresh": "刷新",
809
+ "view.retry": "重试",
810
+ "view.error": "读取失败:{code}",
811
+ "view.root": "本会话",
812
+ "view.live": "在线",
813
+ "view.delegated": "子代理",
814
+ "view.detail.loading": "正在读取传递路径…",
815
+ "view.detail.route": "传递路径",
816
+ "view.detail.routeEmpty": "(还没有记录跳)",
817
+ "view.detail.pending": "仍在等待送达:{targets}",
818
+ "view.detail.body": "正文",
819
+ "view.detail.path": "账本文件",
820
+ "row.hops": "{count} 跳",
821
+ "row.authors": "{count} 位共写",
822
+ "row.filterByTask": "只看任务「{task}」",
823
+ "filter.all": "全部",
824
+ "filter.open": "进行中",
825
+ "filter.acked": "已回执",
826
+ "filter.closed": "已结案",
827
+ "filter.search": "搜索主题、编号、会话或收发…",
828
+ "filter.clear": "清除",
829
+ "filter.showing": "显示 {visible}/{total} 份汇报",
830
+ "filter.none": "没有符合当前筛选的汇报。",
831
+ "filter.hint": "状态筛选只作用于汇报;搜索会同时匹配会话标题与编号。",
832
+ "basis.created": "按创建",
833
+ "basis.updated": "按最近活动",
834
+ "basis.hint": "汇报按创建时间还是按最后一次活动排入时间线;会话始终按创建时间。",
835
+ "detail.thread": "线程",
836
+ "detail.parent": "上溯",
837
+ "detail.children": "下递",
838
+ "detail.openLinked": "打开 {report}",
839
+ "detail.outside": "{report} 不在当前会话树的列表里(它可能属于另一条线);正文与路径仍可读取。",
840
+ "detail.filteredOut": "{report} 在当前筛选下被隐藏了(用「清除」恢复)。",
841
+ "detail.backToList": "回到列表",
842
+ "detail.bodyTruncated": "正文过长,此处只显示开头 {chars} 字;完整内容见账本文件。"
843
+ };
844
+ /** English dictionary, checked against the Chinese key set. */
845
+ const en = {
846
+ "view.tab": "Reports",
847
+ "view.loading": "Reading the ledger…",
848
+ "view.summary": "{sessions} sessions · {reports} reports",
849
+ "view.empty": "No reports in this session tree yet. Once an agent opens one with report_author, its digest card appears here.",
850
+ "view.refresh": "Refresh",
851
+ "view.retry": "Retry",
852
+ "view.error": "Read failed: {code}",
853
+ "view.root": "this session",
854
+ "view.live": "live",
855
+ "view.delegated": "subagent",
856
+ "view.detail.loading": "Reading the transfer path…",
857
+ "view.detail.route": "Transfer path",
858
+ "view.detail.routeEmpty": "(no hops recorded yet)",
859
+ "view.detail.pending": "Still owed to: {targets}",
860
+ "view.detail.body": "Body",
861
+ "view.detail.path": "Ledger file",
862
+ "row.hops": "{count} hops",
863
+ "row.authors": "{count} authors",
864
+ "row.filterByTask": "Show only task \"{task}\"",
865
+ "filter.all": "All",
866
+ "filter.open": "Open",
867
+ "filter.acked": "Acked",
868
+ "filter.closed": "Closed",
869
+ "filter.search": "Search subject, id, session, or people…",
870
+ "filter.clear": "Clear",
871
+ "filter.showing": "Showing {visible}/{total} reports",
872
+ "filter.none": "No reports match the current filter.",
873
+ "filter.hint": "The status filter narrows reports only; search matches every row, including session titles and ids.",
874
+ "basis.created": "By creation",
875
+ "basis.updated": "By activity",
876
+ "basis.hint": "Whether reports are placed at creation or at their latest activity; sessions always keep their creation time.",
877
+ "detail.thread": "Thread",
878
+ "detail.parent": "Answers",
879
+ "detail.children": "Answered by",
880
+ "detail.openLinked": "Open {report}",
881
+ "detail.outside": "{report} is not in this session tree's list (it may belong to another line); its body and path are still readable.",
882
+ "detail.filteredOut": "{report} is hidden by the current filter (use Clear to restore it).",
883
+ "detail.backToList": "Back to the list",
884
+ "detail.bodyTruncated": "Body is long, so only the first {chars} characters are shown here; the ledger file has the whole thing."
885
+ };
886
+ //#endregion
887
+ //#region src/client/index.ts
888
+ /**
889
+ * Browser half of dsh-report-ledger — runs inside the dsh web GUI.
890
+ *
891
+ * The shell serves the built `lib/client.js` from /plugins/dsh-report-ledger/client.js
892
+ * and evaluates it as a closure-factory artifact, so this module's `require` is
893
+ * answered by the loader's frozen module table (react, cordis, the slot
894
+ * services) and every other dependency is inlined by the build.
895
+ *
896
+ * It contributes exactly one thing: a `conversation.view` entry, which the
897
+ * conversation shell projects as a tab beside the shipped Chat and Trajectory
898
+ * views. A fresh id is used rather than an existing one, so nothing shipped is
899
+ * replaced.
900
+ *
901
+ * @module dsh-report-ledger/client
902
+ */
903
+ /** Locale namespace this plugin owns. */
904
+ const NS = "report-ledger";
905
+ /** Tab key. Distinct from the shipped `chat` / `trajectory` cells. */
906
+ const VIEW_ID = "report-ledger";
907
+ /** Position after the shipped views (`chat` 0, `trajectory` 10). */
908
+ const VIEW_ORDER = 20;
909
+ /** The slot and locale services must be mounted before this half can contribute. */
910
+ const inject = ["slots", "locale"];
911
+ /**
912
+ * Apply the browser half.
913
+ * @param ctx - the client plugin context (`slots`, `locale` injected).
914
+ */
915
+ function apply(ctx) {
916
+ ctx.effect(() => ctx.locale.register(NS, {
917
+ zh,
918
+ en
919
+ }), "report-ledger: dictionaries");
920
+ const t = ctx.locale.bind(NS);
921
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
922
+ name: "conversation.view",
923
+ id: VIEW_ID,
924
+ order: VIEW_ORDER,
925
+ label: () => t("view.tab"),
926
+ locale: NS
927
+ }, (props) => (0, react.createElement)(ReportsView, {
928
+ ...props,
929
+ t
930
+ })));
931
+ }
932
+ //#endregion
933
+ exports.apply = apply;
934
+ exports.inject = inject;
935
+ return module.exports;
936
+ }
937
+ });
938
+
939
+ //# sourceMappingURL=client.js.map