iterate-plugin 2.12.2 → 2.12.3

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 CHANGED
@@ -994,6 +994,50 @@ function buildRuntimeStatusGuide() {
994
994
  ];
995
995
  return lines.join("\n");
996
996
  }
997
+ function filterLiveEntries(entries, type) {
998
+ const list = Array.isArray(entries) ? entries : [];
999
+ const t = typeof type === "string" ? type.trim() : "";
1000
+ if (!t) return list.slice();
1001
+ return list.filter((e) => e && typeof e === "object" && String(e.type ?? "") === t);
1002
+ }
1003
+ function filterTimelineEntries(entries, opts) {
1004
+ const list = Array.isArray(entries) ? entries : [];
1005
+ const o = opts && typeof opts === "object" ? opts : {};
1006
+ const type = typeof o.type === "string" ? o.type : "";
1007
+ const round = typeof o.round === "string" ? o.round : "";
1008
+ const q = typeof o.search === "string" ? o.search.trim().toLowerCase() : "";
1009
+ const filtered = list.filter((t) => {
1010
+ if (!t || typeof t !== "object") return false;
1011
+ if (type && String(t.type ?? "") !== type) return false;
1012
+ if (round && String(t.round ?? "") !== round) return false;
1013
+ if (q) {
1014
+ const hay = [String(t.type ?? ""), String(t.round ?? ""), JSON.stringify(t.data ?? {})].join(" ").toLowerCase();
1015
+ if (hay.indexOf(q) < 0) return false;
1016
+ }
1017
+ return true;
1018
+ });
1019
+ return filtered.slice().sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
1020
+ }
1021
+ function serializeObservatoryExport(manifest, live) {
1022
+ const payload = {
1023
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1024
+ manifest: manifest && typeof manifest === "object" ? manifest : null,
1025
+ live: Array.isArray(live) ? live : []
1026
+ };
1027
+ try {
1028
+ return JSON.stringify(payload, null, 2);
1029
+ } catch {
1030
+ return JSON.stringify({ exportedAt: payload.exportedAt, manifest: null, live: [] }, null, 2);
1031
+ }
1032
+ }
1033
+ function latestPhase(phases) {
1034
+ const list = Array.isArray(phases) ? phases : [];
1035
+ let latest = "";
1036
+ for (const p of list) {
1037
+ if (typeof p === "string" && p.trim()) latest = p.trim();
1038
+ }
1039
+ return latest;
1040
+ }
997
1041
 
998
1042
  // src/client/index.ts
999
1043
  var React = require("react");
@@ -1103,6 +1147,11 @@ var ITERATE_CSS = `
1103
1147
  .iterate-filter-search { padding: 4px 8px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-size: 11px; min-width: 140px; }
1104
1148
  .iterate-filter-search::placeholder { color: var(--dsw-alias-label-secondary); }
1105
1149
  .iterate-filter-count { margin-left: auto; white-space: nowrap; }
1150
+ .iterate-filter-chip { padding: 3px 9px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
1151
+ .iterate-filter-chip:hover { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
1152
+ .iterate-filter-chip[data-active] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); font-weight: 600; }
1153
+ .iterate-filter-clear { padding: 3px 9px; border-radius: 999px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent); color: var(--dsw-alias-state-warn-primary); background: transparent; font-size: 11px; cursor: pointer; }
1154
+ .iterate-filter-clear:hover { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent); }
1106
1155
  .iterate-batch { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); }
1107
1156
  .iterate-batch-label { margin-right: 2px; }
1108
1157
  .iterate-batch-btn { padding: 3px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
@@ -1155,6 +1204,9 @@ var ITERATE_CSS = `
1155
1204
  /* Interruption / resume + attachment chips (dashboard) */
1156
1205
  .iterate-chip-resume { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
1157
1206
  .iterate-chip-images { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
1207
+ /* Live workflow-phase chip (dashboard) */
1208
+ .iterate-chip-phase { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-label-primary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 14%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 26%, transparent); }
1209
+ .iterate-chip-phase[data-live] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
1158
1210
 
1159
1211
  /* Accessibility-switch toggle */
1160
1212
  .iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
@@ -1293,6 +1345,24 @@ function copyText(text) {
1293
1345
  }
1294
1346
  return Promise.resolve(false);
1295
1347
  }
1348
+ function downloadTextFile(filename, text) {
1349
+ if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") return false;
1350
+ try {
1351
+ const blob = new Blob([text], { type: "application/json;charset=utf-8" });
1352
+ const url = URL.createObjectURL(blob);
1353
+ const a = document.createElement("a");
1354
+ a.href = url;
1355
+ a.download = filename;
1356
+ a.rel = "noopener";
1357
+ document.body.appendChild(a);
1358
+ a.click();
1359
+ document.body.removeChild(a);
1360
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1361
+ return true;
1362
+ } catch {
1363
+ return false;
1364
+ }
1365
+ }
1296
1366
  var SEVERITY_KEYS = ["critical", "high", "medium", "low"];
1297
1367
  function coerceSeverity(severity) {
1298
1368
  return SEVERITY_KEYS.includes(severity ?? "") ? severity : "low";
@@ -1415,6 +1485,14 @@ function ConvergenceDashboard(props) {
1415
1485
  key: "images",
1416
1486
  title: "\u4F1A\u8BDD\u4E2D\u68C0\u6D4B\u5230\u7528\u6237\u9644\u5E26\u7684\u56FE\u7247\uFF0C\u8BC4\u5BA1\u5C06\u4F5C\u4E3A\u89C6\u89C9\u8BC1\u636E\u53C2\u8003"
1417
1487
  }, `\u9644\u4EF6\u56FE\u7247 ${String(imageCount)}`) : null;
1488
+ const transcript = latestTranscript(session);
1489
+ const phase = transcript ? latestPhase(transcript.phases) : "";
1490
+ const liveChip = transcript && phase ? React.createElement("span", {
1491
+ className: "iterate-chip-phase",
1492
+ key: "phase",
1493
+ "data-live": transcript.active === true ? "" : void 0,
1494
+ title: transcript.active === true ? `\u5F53\u524D\u5904\u4E8E\u300C${phase}\u300D\u9636\u6BB5\uFF08\u8FD0\u884C\u4E2D\uFF09` : `\u6700\u8FD1\u4E00\u6B21\u6267\u884C\u505C\u7559\u5728\u300C${phase}\u300D\u9636\u6BB5\uFF08\u5DF2\u7ED3\u675F\uFF09`
1495
+ }, `${phase} \xB7 ${transcript.active === true ? "\u8FD0\u884C\u4E2D" : "\u5DF2\u7ED3\u675F"}`) : null;
1418
1496
  const dimNames = Object.keys(dims);
1419
1497
  const dimBadges = dimNames.slice(0, 6).map(
1420
1498
  (dim) => React.createElement(
@@ -1478,6 +1556,7 @@ function ConvergenceDashboard(props) {
1478
1556
  fixBadge,
1479
1557
  resumeChip,
1480
1558
  imageChip,
1559
+ liveChip,
1481
1560
  React.createElement(TrendChart, { points: trend.points }),
1482
1561
  ...dimBadges
1483
1562
  );
@@ -2150,11 +2229,17 @@ function ObservatoryPanel(props) {
2150
2229
  const [open, setOpen] = React.useState(false);
2151
2230
  const [tab, setTab] = React.useState("live");
2152
2231
  const [expandedThreads, setExpandedThreads] = React.useState(/* @__PURE__ */ new Set());
2232
+ const [threadMode, setThreadMode] = React.useState("auto");
2153
2233
  const [copiedKey, setCopiedKey] = React.useState(null);
2154
2234
  const [copyFailText, setCopyFailText] = React.useState(null);
2155
2235
  const [nudgeText, setNudgeText] = React.useState("");
2156
2236
  const [timelineType, setTimelineType] = React.useState("");
2237
+ const [timelineRound, setTimelineRound] = React.useState("");
2157
2238
  const [timelineSearch, setTimelineSearch] = React.useState("");
2239
+ const [liveType, setLiveType] = React.useState("");
2240
+ const [f3Filter, setF3Filter] = React.useState(
2241
+ { severities: [], dimensions: [], search: "" }
2242
+ );
2158
2243
  const copyTimer = React.useRef(null);
2159
2244
  React.useEffect(() => () => {
2160
2245
  if (copyTimer.current) clearTimeout(copyTimer.current);
@@ -2172,6 +2257,7 @@ function ObservatoryPanel(props) {
2172
2257
  });
2173
2258
  };
2174
2259
  const toggleThread = (key) => {
2260
+ setThreadMode("auto");
2175
2261
  setExpandedThreads((prev) => {
2176
2262
  const next = new Set(prev);
2177
2263
  if (next.has(key)) next.delete(key);
@@ -2179,6 +2265,35 @@ function ObservatoryPanel(props) {
2179
2265
  return next;
2180
2266
  });
2181
2267
  };
2268
+ const exportObservatory = () => {
2269
+ const json = serializeObservatoryExport(manifest, manifest.live || []);
2270
+ const filename = `iterate-observatory-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2271
+ const flash = () => {
2272
+ setCopiedKey("export");
2273
+ if (copyTimer.current) clearTimeout(copyTimer.current);
2274
+ copyTimer.current = setTimeout(() => setCopiedKey((cur) => cur === "export" ? null : cur), 1600);
2275
+ };
2276
+ if (downloadTextFile(filename, json)) {
2277
+ flash();
2278
+ return;
2279
+ }
2280
+ copyText(json).then((ok) => {
2281
+ if (!ok) {
2282
+ setCopyFailText(json);
2283
+ return;
2284
+ }
2285
+ flash();
2286
+ });
2287
+ };
2288
+ const setAllThreads = (mode) => {
2289
+ setThreadMode(mode);
2290
+ setExpandedThreads(/* @__PURE__ */ new Set());
2291
+ };
2292
+ const isThreadExpanded = (key) => {
2293
+ if (threadMode === "all") return true;
2294
+ if (threadMode === "none") return false;
2295
+ return expandedThreads.has(key);
2296
+ };
2182
2297
  if (!manifest) {
2183
2298
  return React.createElement(
2184
2299
  "div",
@@ -2211,69 +2326,85 @@ function ObservatoryPanel(props) {
2211
2326
  if (rounds.length === 0) {
2212
2327
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u5BA1\u67E5\u7EBF\u7A0B");
2213
2328
  }
2214
- return React.createElement("div", {}, ...rounds.map((r, ri) => {
2215
- const threads = r.threads || [];
2216
- const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0);
2217
- const threadBlocks = threads.length === 0 ? [React.createElement("div", { key: "none", className: "iterate-obs-empty" }, "\u672C\u8F6E\u65E0\u7EBF\u7A0B")] : threads.map((t, ti) => {
2218
- const key = `${ri}-${ti}`;
2219
- const expanded = expandedThreads.has(key);
2220
- const dim = t.dimension || "\u672A\u547D\u540D\u7EF4\u5EA6";
2221
- const tFindings = t.findings || [];
2222
- const files = t.readFiles || [];
2223
- const msgs = t.messages || [];
2224
- return React.createElement(
2225
- "div",
2226
- { key, className: "iterate-obs-block" },
2227
- React.createElement(
2228
- "div",
2229
- { className: "iterate-obs-block-head", "data-click": "", onClick: () => toggleThread(key), title: expanded ? "\u6536\u8D77\u7EBF\u7A0B" : "\u5C55\u5F00\u7EBF\u7A0B" },
2230
- React.createElement("span", {}, expanded ? "\u2212" : "+"),
2231
- React.createElement("b", {}, dim),
2232
- React.createElement("span", { className: "iterate-obs-chip" }, `attempt ${typeof t.attempt === "number" ? t.attempt : "?"}`),
2233
- React.createElement("span", { className: "iterate-obs-chip" }, `${tFindings.length} findings`)
2234
- ),
2235
- expanded ? React.createElement(
2329
+ return React.createElement(
2330
+ "div",
2331
+ {},
2332
+ React.createElement(
2333
+ "div",
2334
+ { className: "iterate-obs-bar", style: { marginBottom: 8, flexWrap: "wrap" } },
2335
+ React.createElement("b", {}, "\u5BA1\u67E5\u7EBF\u7A0B"),
2336
+ React.createElement("button", { className: "iterate-btn", onClick: () => setAllThreads("all"), title: "\u5C55\u5F00\u5168\u90E8\u7EBF\u7A0B" }, "\u5168\u90E8\u5C55\u5F00"),
2337
+ React.createElement("button", { className: "iterate-btn", onClick: () => setAllThreads("none"), title: "\u6536\u8D77\u5168\u90E8\u7EBF\u7A0B" }, "\u5168\u90E8\u6536\u8D77"),
2338
+ React.createElement(
2339
+ "span",
2340
+ { className: "iterate-filter-count" },
2341
+ `${threadMode === "all" ? "\u5168\u90E8\u5C55\u5F00" : threadMode === "none" ? "\u5168\u90E8\u6536\u8D77" : "\u81EA\u7531\u5207\u6362"} \xB7 ${rounds.length} \u8F6E`
2342
+ )
2343
+ ),
2344
+ ...rounds.map((r, ri) => {
2345
+ const threads = r.threads || [];
2346
+ const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0);
2347
+ const threadBlocks = threads.length === 0 ? [React.createElement("div", { key: "none", className: "iterate-obs-empty" }, "\u672C\u8F6E\u65E0\u7EBF\u7A0B")] : threads.map((t, ti) => {
2348
+ const key = `${ri}-${ti}`;
2349
+ const expanded = isThreadExpanded(key);
2350
+ const dim = t.dimension || "\u672A\u547D\u540D\u7EF4\u5EA6";
2351
+ const tFindings = t.findings || [];
2352
+ const files = t.readFiles || [];
2353
+ const msgs = t.messages || [];
2354
+ return React.createElement(
2236
2355
  "div",
2237
- { className: "iterate-obs-block-body" },
2356
+ { key, className: "iterate-obs-block" },
2238
2357
  React.createElement(
2239
2358
  "div",
2240
- { className: "iterate-obs-bar" },
2241
- React.createElement("b", {}, "\u53D9\u8FF0"),
2242
- React.createElement("span", { className: "iterate-obs-head-meta" }, `${msgs.length} \u6761`)
2359
+ { className: "iterate-obs-block-head", "data-click": "", onClick: () => toggleThread(key), title: expanded ? "\u6536\u8D77\u7EBF\u7A0B" : "\u5C55\u5F00\u7EBF\u7A0B" },
2360
+ React.createElement("span", {}, expanded ? "\u2212" : "+"),
2361
+ React.createElement("b", {}, dim),
2362
+ React.createElement("span", { className: "iterate-obs-chip" }, `attempt ${typeof t.attempt === "number" ? t.attempt : "?"}`),
2363
+ React.createElement("span", { className: "iterate-obs-chip" }, `${tFindings.length} findings`)
2243
2364
  ),
2244
- ...msgs.length === 0 ? [React.createElement("div", { key: "msg-none", className: "iterate-obs-empty" }, "\u65E0\u7EBF\u7D22\u53D9\u8FF0")] : msgs.map((m, mi) => React.createElement("div", { key: mi, className: "iterate-obs-msg" }, String(m))),
2245
- ...files.length > 0 ? [
2365
+ expanded ? React.createElement(
2366
+ "div",
2367
+ { className: "iterate-obs-block-body" },
2246
2368
  React.createElement(
2247
2369
  "div",
2248
- { key: "files-h", className: "iterate-obs-bar", style: { marginTop: 6 } },
2249
- React.createElement("b", {}, "\u8BFB\u53D6\u6587\u4EF6")
2370
+ { className: "iterate-obs-bar" },
2371
+ React.createElement("b", {}, "\u53D9\u8FF0"),
2372
+ React.createElement("span", { className: "iterate-obs-head-meta" }, `${msgs.length} \u6761`)
2250
2373
  ),
2251
- ...files.map(
2252
- (f, fi) => React.createElement("div", { key: `f${fi}`, className: "iterate-obs-file" }, String(f))
2253
- )
2254
- ] : [],
2255
- React.createElement(
2256
- "div",
2257
- { className: "iterate-obs-bar", style: { marginTop: 6 } },
2258
- React.createElement("b", {}, "\u53D1\u73B0")
2259
- ),
2260
- ...tFindings.length === 0 ? [React.createElement("div", { key: "finding-none", className: "iterate-obs-empty" }, "\u672C\u7EBF\u7A0B\u65E0\u53D1\u73B0")] : tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))
2261
- ) : null
2262
- );
2263
- });
2264
- return React.createElement(
2265
- "div",
2266
- { key: ri, className: "iterate-obs-block" },
2267
- React.createElement(
2374
+ ...msgs.length === 0 ? [React.createElement("div", { key: "msg-none", className: "iterate-obs-empty" }, "\u65E0\u7EBF\u7D22\u53D9\u8FF0")] : msgs.map((m, mi) => React.createElement("div", { key: mi, className: "iterate-obs-msg" }, String(m))),
2375
+ ...files.length > 0 ? [
2376
+ React.createElement(
2377
+ "div",
2378
+ { key: "files-h", className: "iterate-obs-bar", style: { marginTop: 6 } },
2379
+ React.createElement("b", {}, "\u8BFB\u53D6\u6587\u4EF6")
2380
+ ),
2381
+ ...files.map(
2382
+ (f, fi) => React.createElement("div", { key: `f${fi}`, className: "iterate-obs-file" }, String(f))
2383
+ )
2384
+ ] : [],
2385
+ React.createElement(
2386
+ "div",
2387
+ { className: "iterate-obs-bar", style: { marginTop: 6 } },
2388
+ React.createElement("b", {}, "\u53D1\u73B0")
2389
+ ),
2390
+ ...tFindings.length === 0 ? [React.createElement("div", { key: "finding-none", className: "iterate-obs-empty" }, "\u672C\u7EBF\u7A0B\u65E0\u53D1\u73B0")] : tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))
2391
+ ) : null
2392
+ );
2393
+ });
2394
+ return React.createElement(
2268
2395
  "div",
2269
- { className: "iterate-obs-block-head" },
2270
- React.createElement("b", {}, `Round ${typeof r.round === "number" ? r.round : ri + 1}`),
2271
- React.createElement("span", { className: "iterate-obs-chip" }, `${threads.length} \u7EBF\u7A0B`),
2272
- React.createElement("span", { className: "iterate-obs-head-meta" }, `${fCount} findings`)
2273
- ),
2274
- React.createElement("div", { className: "iterate-obs-block-body" }, ...threadBlocks)
2275
- );
2276
- }));
2396
+ { key: ri, className: "iterate-obs-block" },
2397
+ React.createElement(
2398
+ "div",
2399
+ { className: "iterate-obs-block-head" },
2400
+ React.createElement("b", {}, `Round ${typeof r.round === "number" ? r.round : ri + 1}`),
2401
+ React.createElement("span", { className: "iterate-obs-chip" }, `${threads.length} \u7EBF\u7A0B`),
2402
+ React.createElement("span", { className: "iterate-obs-head-meta" }, `${fCount} findings`)
2403
+ ),
2404
+ React.createElement("div", { className: "iterate-obs-block-body" }, ...threadBlocks)
2405
+ );
2406
+ })
2407
+ );
2277
2408
  };
2278
2409
  const renderTrend = () => {
2279
2410
  const conv = manifest.convergence || [];
@@ -2297,7 +2428,60 @@ function ObservatoryPanel(props) {
2297
2428
  if (findings.length === 0) {
2298
2429
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u53D1\u73B0");
2299
2430
  }
2300
- return React.createElement("div", {}, ...findings.map((f, i) => {
2431
+ const opts = buildFilterOptions(findings);
2432
+ const { filtered } = filterFindingsWithIndices(findings, f3Filter);
2433
+ const filterActive = f3Filter.severities.length > 0 || f3Filter.dimensions.length > 0 || f3Filter.search.trim() !== "";
2434
+ const toggleSeverity = (sev) => {
2435
+ setF3Filter((prev) => ({
2436
+ ...prev,
2437
+ severities: prev.severities.includes(sev) ? prev.severities.filter((s) => s !== sev) : [...prev.severities, sev]
2438
+ }));
2439
+ };
2440
+ const filterBar = React.createElement(
2441
+ "div",
2442
+ { className: "iterate-obs-bar", style: { marginBottom: 8, flexWrap: "wrap" } },
2443
+ React.createElement("b", {}, "\u53D1\u73B0"),
2444
+ ...opts.severities.map(
2445
+ (s) => React.createElement("button", {
2446
+ key: `sev-${s.value}`,
2447
+ className: "iterate-filter-chip",
2448
+ "data-active": f3Filter.severities.includes(s.value) ? "" : void 0,
2449
+ title: `${s.value}\uFF08${s.count} \u9879\uFF09`,
2450
+ onClick: () => toggleSeverity(s.value)
2451
+ }, `${s.value} ${s.count}`)
2452
+ ),
2453
+ React.createElement(
2454
+ "select",
2455
+ {
2456
+ className: "iterate-filter-select",
2457
+ value: f3Filter.dimensions[0] || "",
2458
+ "aria-label": "\u6309\u7EF4\u5EA6\u7B5B\u9009",
2459
+ onChange: (e) => {
2460
+ const v = e.target.value;
2461
+ setF3Filter((prev) => ({ ...prev, dimensions: v ? [v] : [] }));
2462
+ }
2463
+ },
2464
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u7EF4\u5EA6"),
2465
+ ...opts.dimensions.map(
2466
+ (d) => React.createElement("option", { key: d.value, value: d.value }, `${d.value} (${d.count})`)
2467
+ )
2468
+ ),
2469
+ React.createElement("input", {
2470
+ className: "iterate-filter-search",
2471
+ type: "search",
2472
+ placeholder: "\u641C\u7D22\u53D1\u73B0\u2026",
2473
+ "aria-label": "\u641C\u7D22\u53D1\u73B0",
2474
+ value: f3Filter.search,
2475
+ onChange: (e) => setF3Filter((prev) => ({ ...prev, search: e.target.value }))
2476
+ }),
2477
+ filterActive ? React.createElement("button", {
2478
+ className: "iterate-filter-clear",
2479
+ onClick: () => setF3Filter({ severities: [], dimensions: [], search: "" }),
2480
+ title: "\u6E05\u9664\u5168\u90E8\u7B5B\u9009"
2481
+ }, "\u6E05\u9664\u7B5B\u9009") : null,
2482
+ React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${findings.length}`)
2483
+ );
2484
+ const findingBlocks = filtered.map((f, i) => {
2301
2485
  const k = `f3-${i}`;
2302
2486
  const loc = `${String(f.file || "?")}${typeof f.line === "number" && f.line > 0 ? `:${f.line}` : ""}`;
2303
2487
  const entry = {
@@ -2349,7 +2533,16 @@ function ObservatoryPanel(props) {
2349
2533
  )
2350
2534
  )
2351
2535
  );
2352
- }));
2536
+ });
2537
+ if (filtered.length === 0) {
2538
+ return React.createElement(
2539
+ "div",
2540
+ {},
2541
+ filterBar,
2542
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u53D1\u73B0")
2543
+ );
2544
+ }
2545
+ return React.createElement("div", {}, filterBar, ...findingBlocks);
2353
2546
  };
2354
2547
  const renderFixes = () => {
2355
2548
  const fixes = manifest.fixes || [];
@@ -2515,14 +2708,21 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2515
2708
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u65F6\u95F4\u7EBF\u6761\u76EE");
2516
2709
  }
2517
2710
  const types = Array.from(new Set(entries.map((t) => String(t.type || "unknown")).filter(Boolean))).sort();
2518
- const q = timelineSearch.trim().toLowerCase();
2519
- const filtered = entries.filter((t) => {
2520
- if (timelineType && String(t.type || "") !== timelineType) return false;
2521
- if (!q) return true;
2522
- const hay = [String(t.type || ""), String(t.round ?? ""), JSON.stringify(t.data || {})].join(" ").toLowerCase();
2523
- return hay.indexOf(q) >= 0;
2711
+ const rounds = Array.from(
2712
+ new Set(entries.map((t) => typeof t.round === "number" ? String(t.round) : "").filter(Boolean))
2713
+ ).sort((a, b) => Number(a) - Number(b));
2714
+ const filtered = filterTimelineEntries(entries, {
2715
+ type: timelineType,
2716
+ round: timelineRound,
2717
+ search: timelineSearch
2524
2718
  });
2525
- const sorted = filtered.slice().sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")));
2719
+ const filterActive = Boolean(timelineType || timelineRound || timelineSearch.trim());
2720
+ const clearTimelineFilter = () => {
2721
+ setTimelineType("");
2722
+ setTimelineRound("");
2723
+ setTimelineSearch("");
2724
+ };
2725
+ const sorted = filtered.slice();
2526
2726
  const rows = sorted.map((t, i) => {
2527
2727
  const k = `f7-${i}`;
2528
2728
  const dataText = t.data && typeof t.data === "object" ? JSON.stringify(t.data) : "";
@@ -2542,38 +2742,56 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2542
2742
  dataText ? React.createElement("div", { className: "iterate-obs-code" }, dataText) : null
2543
2743
  );
2544
2744
  });
2545
- if (rows.length === 0) {
2546
- return React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u65F6\u95F4\u7EBF\u6761\u76EE");
2547
- }
2548
- return React.createElement(
2745
+ const filterBar = React.createElement(
2549
2746
  "div",
2550
- {},
2747
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2748
+ React.createElement("b", {}, "\u65F6\u95F4\u7EBF"),
2551
2749
  React.createElement(
2552
- "div",
2553
- { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2554
- React.createElement(
2555
- "select",
2556
- {
2557
- className: "iterate-filter-select",
2558
- value: timelineType,
2559
- "aria-label": "\u6309\u65F6\u95F4\u7EBF\u7C7B\u578B\u7B5B\u9009",
2560
- onChange: (e) => setTimelineType(e.target.value)
2561
- },
2562
- React.createElement("option", { value: "" }, "\u5168\u90E8\u7C7B\u578B"),
2563
- ...types.map((t) => React.createElement("option", { key: t, value: t }, t))
2564
- ),
2565
- React.createElement("input", {
2566
- className: "iterate-filter-search",
2567
- type: "search",
2568
- placeholder: "\u641C\u7D22\u65F6\u95F4\u7EBF\u2026",
2569
- "aria-label": "\u641C\u7D22\u65F6\u95F4\u7EBF",
2570
- value: timelineSearch,
2571
- onChange: (e) => setTimelineSearch(e.target.value)
2572
- }),
2573
- React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${entries.length}`)
2750
+ "select",
2751
+ {
2752
+ className: "iterate-filter-select",
2753
+ value: timelineType,
2754
+ "aria-label": "\u6309\u65F6\u95F4\u7EBF\u7C7B\u578B\u7B5B\u9009",
2755
+ onChange: (e) => setTimelineType(e.target.value)
2756
+ },
2757
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u7C7B\u578B"),
2758
+ ...types.map((t) => React.createElement("option", { key: t, value: t }, t))
2574
2759
  ),
2575
- ...rows
2760
+ React.createElement(
2761
+ "select",
2762
+ {
2763
+ className: "iterate-filter-select",
2764
+ value: timelineRound,
2765
+ "aria-label": "\u6309\u8F6E\u6B21\u7B5B\u9009",
2766
+ onChange: (e) => setTimelineRound(e.target.value)
2767
+ },
2768
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u8F6E\u6B21"),
2769
+ ...rounds.map((r) => React.createElement("option", { key: r, value: r }, `Round ${r}`))
2770
+ ),
2771
+ React.createElement("input", {
2772
+ className: "iterate-filter-search",
2773
+ type: "search",
2774
+ placeholder: "\u641C\u7D22\u65F6\u95F4\u7EBF\u2026",
2775
+ "aria-label": "\u641C\u7D22\u65F6\u95F4\u7EBF",
2776
+ value: timelineSearch,
2777
+ onChange: (e) => setTimelineSearch(e.target.value)
2778
+ }),
2779
+ filterActive ? React.createElement("button", {
2780
+ className: "iterate-filter-clear",
2781
+ onClick: clearTimelineFilter,
2782
+ title: "\u6E05\u9664\u5168\u90E8\u7B5B\u9009"
2783
+ }, "\u6E05\u9664") : null,
2784
+ React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${entries.length}`)
2576
2785
  );
2786
+ if (rows.length === 0) {
2787
+ return React.createElement(
2788
+ "div",
2789
+ {},
2790
+ filterBar,
2791
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u65F6\u95F4\u7EBF\u6761\u76EE")
2792
+ );
2793
+ }
2794
+ return React.createElement("div", {}, filterBar, ...rows);
2577
2795
  };
2578
2796
  const renderLive = () => {
2579
2797
  const liveEntries = manifest.live || [];
@@ -2595,7 +2813,9 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2595
2813
  }
2596
2814
  return React.createElement("div", { className: "iterate-obs-empty" }, "\u6682\u65E0\u5B9E\u65F6\u6D3B\u52A8");
2597
2815
  }
2598
- const rows = liveEntries.map((e, i) => {
2816
+ const types = Array.from(new Set(liveEntries.map((e) => String(e.type || "unknown")).filter(Boolean))).sort();
2817
+ const filtered = filterLiveEntries(liveEntries, liveType);
2818
+ const rows = filtered.map((e, i) => {
2599
2819
  const type = String(e.type || "unknown");
2600
2820
  const meta = OBS_LIVE_META[type] || { label: String(e.type || "?"), color: OBS_LIVE_FALLBACK_COLOR };
2601
2821
  const tool = String(e.tool || "");
@@ -2610,17 +2830,37 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2610
2830
  React.createElement("span", { className: "iterate-obs-head-meta" }, time)
2611
2831
  );
2612
2832
  });
2613
- return React.createElement(
2833
+ const filterBar = React.createElement(
2614
2834
  "div",
2615
- {},
2835
+ { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2836
+ React.createElement("b", {}, "\u5B9E\u65F6\u6D3B\u52A8"),
2616
2837
  React.createElement(
2617
- "div",
2618
- { className: "iterate-obs-bar", style: { marginBottom: 8 } },
2619
- React.createElement("b", {}, "\u5B9E\u65F6\u6D3B\u52A8"),
2620
- React.createElement("span", { className: "iterate-obs-chip" }, `${liveEntries.length} \u9879`)
2838
+ "select",
2839
+ {
2840
+ className: "iterate-filter-select",
2841
+ value: liveType,
2842
+ "aria-label": "\u6309\u6D3B\u52A8\u7C7B\u578B\u7B5B\u9009",
2843
+ onChange: (e) => setLiveType(e.target.value)
2844
+ },
2845
+ React.createElement("option", { value: "" }, "\u5168\u90E8\u6D3B\u52A8"),
2846
+ ...types.map((t) => React.createElement("option", { key: t, value: t }, t))
2621
2847
  ),
2622
- ...rows
2848
+ liveType ? React.createElement("button", {
2849
+ className: "iterate-filter-clear",
2850
+ onClick: () => setLiveType(""),
2851
+ title: "\u6E05\u9664\u7C7B\u578B\u7B5B\u9009"
2852
+ }, "\u6E05\u9664") : null,
2853
+ React.createElement("span", { className: "iterate-filter-count" }, `${filtered.length}/${liveEntries.length}`)
2623
2854
  );
2855
+ if (rows.length === 0) {
2856
+ return React.createElement(
2857
+ "div",
2858
+ {},
2859
+ filterBar,
2860
+ React.createElement("div", { className: "iterate-obs-empty" }, "\u65E0\u5339\u914D\u7684\u5B9E\u65F6\u6D3B\u52A8")
2861
+ );
2862
+ }
2863
+ return React.createElement("div", {}, filterBar, ...rows);
2624
2864
  };
2625
2865
  const renderBody = () => {
2626
2866
  switch (tab) {
@@ -2682,6 +2922,22 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
2682
2922
  ),
2683
2923
  React.createElement("div", { className: "iterate-obs-code" }, copyFailText)
2684
2924
  ) : null,
2925
+ React.createElement(
2926
+ "div",
2927
+ { className: "iterate-obs-bar", style: { marginBottom: 8, flexWrap: "wrap" } },
2928
+ React.createElement(
2929
+ "span",
2930
+ { className: "iterate-obs-head-meta" },
2931
+ `${manifest.mode || "runtime"} \xB7 ${String(manifest.updatedAt || "")}`
2932
+ ),
2933
+ React.createElement("button", {
2934
+ className: "iterate-btn",
2935
+ "data-primary": "",
2936
+ "data-copied": copiedKey === "export" ? "" : void 0,
2937
+ onClick: exportObservatory,
2938
+ title: "\u5C06\u89C2\u6D4B\u53F0\u5168\u90E8\u6570\u636E\u5BFC\u51FA\u4E3A JSON\uFF08\u4F18\u5148\u4E0B\u8F7D\uFF0C\u5931\u8D25\u5219\u590D\u5236\uFF09"
2939
+ }, copiedKey === "export" ? "\u5DF2\u5BFC\u51FA" : "\u5BFC\u51FA JSON")
2940
+ ),
2685
2941
  React.createElement(
2686
2942
  "div",
2687
2943
  { className: "iterate-obs-tabs" },