markform 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +27 -2
  2. package/dist/ai-sdk.d.mts +1 -2
  3. package/dist/ai-sdk.mjs +2 -2
  4. package/dist/ai-sdk.mjs.map +1 -1
  5. package/dist/{apply-BYgtU64w.mjs → apply-DIvm1b1s.mjs} +430 -31
  6. package/dist/apply-DIvm1b1s.mjs.map +1 -0
  7. package/dist/bin.mjs +20 -2
  8. package/dist/bin.mjs.map +1 -1
  9. package/dist/{cli-D9w0Bp4J.mjs → cli-FFMoEhFS.mjs} +1039 -89
  10. package/dist/cli-FFMoEhFS.mjs.map +1 -0
  11. package/dist/cli.mjs +1 -1
  12. package/dist/{coreTypes-SDB3KRRJ.mjs → coreTypes-CPKXf2dc.mjs} +1 -1
  13. package/dist/{coreTypes-SDB3KRRJ.mjs.map → coreTypes-CPKXf2dc.mjs.map} +1 -1
  14. package/dist/{coreTypes-BMEs8h_2.d.mts → coreTypes-CkxML8g2.d.mts} +4 -9
  15. package/dist/index.d.mts +515 -22
  16. package/dist/index.mjs +5 -5
  17. package/dist/{session-Ci4B0Pna.mjs → session-CK0x28RO.mjs} +2 -2
  18. package/dist/session-CK0x28RO.mjs.map +1 -0
  19. package/dist/{session-CW9AQw6i.mjs → session-ZHBi3LVQ.mjs} +1 -1
  20. package/dist/{shared-fUKfJ1UA.mjs → shared-BTR35aMz.mjs} +1 -1
  21. package/dist/{shared-CCq4haEV.mjs → shared-DwdyWmvE.mjs} +1 -3
  22. package/dist/shared-DwdyWmvE.mjs.map +1 -0
  23. package/dist/{src-DDxi-2ne.mjs → src-wR7GoftB.mjs} +1707 -645
  24. package/dist/src-wR7GoftB.mjs.map +1 -0
  25. package/docs/markform-apis.md +81 -0
  26. package/docs/markform-reference.md +15 -1
  27. package/package.json +17 -15
  28. package/dist/apply-BYgtU64w.mjs.map +0 -1
  29. package/dist/cli-D9w0Bp4J.mjs.map +0 -1
  30. package/dist/session-Ci4B0Pna.mjs.map +0 -1
  31. package/dist/shared-CCq4haEV.mjs.map +0 -1
  32. package/dist/src-DDxi-2ne.mjs.map +0 -1
@@ -1,16 +1,16 @@
1
1
 
2
- import { L as PatchSchema } from "./coreTypes-SDB3KRRJ.mjs";
3
- import { B as WEB_SEARCH_CONFIG, D as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, F as deriveReportPath, I as deriveSchemaPath, L as detectFileType, M as REPORT_EXTENSION, N as USER_ROLE, O as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, P as deriveExportPath, R as parseRolesFlag, S as DEFAULT_MAX_PATCHES_PER_TURN, T as DEFAULT_PORT, U as hasWebSearchSupport, V as formatSuggestedLlms, W as parseModelIdForDisplay, _ as validateSyntaxConsistency, b as DEFAULT_MAX_ISSUES_PER_TURN, d as serializeForm, f as serializeRawMarkdown, i as inspect, j as MAX_FORMS_IN_MENU, m as friendlyUrlAbbrev, n as getAllFields, p as serializeReport, t as applyPatches, v as AGENT_ROLE, w as DEFAULT_MAX_TURNS, y as DEFAULT_FORMS_DIR, z as SUGGESTED_LLMS } from "./apply-BYgtU64w.mjs";
4
- import { A as formToJsonSchema, _ as createMockAgent, a as resolveHarnessConfig, d as getProviderNames, f as resolveModel, h as computeExecutionPlan, i as runResearch, j as parseForm, m as createLiveAgent, n as isResearchForm, o as fillForm, p as buildMockWireFormat, t as VERSION, u as getProviderInfo, y as createHarness } from "./src-DDxi-2ne.mjs";
5
- import { n as serializeSession } from "./session-Ci4B0Pna.mjs";
6
- import { _ as writeFile, a as formatPath, c as logError, d as logTiming, f as logVerbose, g as stripHtmlComments, h as shouldUseColors, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-CCq4haEV.mjs";
2
+ import { L as PatchSchema } from "./coreTypes-CPKXf2dc.mjs";
3
+ import { $ as SUGGESTED_LLMS, B as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, F as DEFAULT_MAX_PATCHES_PER_TURN, G as REPORT_EXTENSION, J as deriveFillRecordPath, K as USER_ROLE, L as DEFAULT_MAX_TURNS, M as DEFAULT_FORMS_DIR, N as DEFAULT_MAX_ISSUES_PER_TURN, Q as parseRolesFlag, R as DEFAULT_PORT, V as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, W as MAX_FORMS_IN_MENU, X as deriveSchemaPath, Y as deriveReportPath, Z as detectFileType, c as computeProgressSummary, d as serializeForm, et as WEB_SEARCH_CONFIG, f as serializeRawMarkdown, h as friendlyUrlAbbrev, i as inspect, it as parseModelIdForDisplay, j as AGENT_ROLE, l as computeStructureSummary, m as formatBareUrlsAsHtmlLinks, n as getAllFields, p as serializeReport, q as deriveExportPath, rt as hasWebSearchSupport, t as applyPatches, tt as formatSuggestedLlms, v as validateSyntaxConsistency } from "./apply-DIvm1b1s.mjs";
4
+ import { C as resolveModel, D as computeExecutionPlan, E as FillRecordCollector, H as formToJsonSchema, S as getProviderNames, T as createLiveAgent, U as parseForm, _ as fillForm, g as resolveHarnessConfig, h as formatFillRecordSummary, i as runResearch, j as createHarness, k as createMockAgent, m as stripUnstableFillRecordFields, n as isResearchForm, t as VERSION, w as buildMockWireFormat, x as getProviderInfo } from "./src-wR7GoftB.mjs";
5
+ import { n as serializeSession } from "./session-CK0x28RO.mjs";
6
+ import { _ as writeFile, a as formatPath, c as logError, d as logTiming, f as logVerbose, g as stripHtmlComments, h as shouldUseColors, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-DwdyWmvE.mjs";
7
7
  import Markdoc from "@markdoc/markdoc";
8
8
  import YAML from "yaml";
9
9
  import { Command } from "commander";
10
10
  import pc from "picocolors";
11
11
  import { exec, execSync, spawn } from "node:child_process";
12
12
  import { basename, dirname, join, resolve } from "node:path";
13
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
13
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { readFile } from "node:fs/promises";
16
16
  import * as p from "@clack/prompts";
@@ -253,12 +253,12 @@ function registerApplyCommand(program) {
253
253
  const patches = parsedJson;
254
254
  const validatedPatches = [];
255
255
  for (let i = 0; i < patches.length; i++) {
256
- const result$1 = PatchSchema.safeParse(patches[i]);
257
- if (!result$1.success) {
258
- logError(`Invalid patch at index ${i}: ${result$1.error.issues[0]?.message ?? "Unknown error"}`);
256
+ const result = PatchSchema.safeParse(patches[i]);
257
+ if (!result.success) {
258
+ logError(`Invalid patch at index ${i}: ${result.error.issues[0]?.message ?? "Unknown error"}`);
259
259
  process.exit(1);
260
260
  }
261
- validatedPatches.push(result$1.data);
261
+ validatedPatches.push(result.data);
262
262
  }
263
263
  if (ctx.dryRun) {
264
264
  logDryRun(`Would apply ${validatedPatches.length} patches to ${file}`, { patches: validatedPatches });
@@ -418,7 +418,7 @@ async function displayWithPager(content, title) {
418
418
  return;
419
419
  }
420
420
  const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
421
- return new Promise((resolve$1) => {
421
+ return new Promise((resolve) => {
422
422
  const pager = spawn("less", [
423
423
  "-R",
424
424
  "-S",
@@ -435,10 +435,10 @@ async function displayWithPager(content, title) {
435
435
  console.log("");
436
436
  console.log(content);
437
437
  console.log("");
438
- resolve$1();
438
+ resolve();
439
439
  });
440
440
  pager.on("close", () => {
441
- resolve$1();
441
+ resolve();
442
442
  });
443
443
  pager.stdin.write(header + "\n\n");
444
444
  pager.stdin.write(content);
@@ -1353,13 +1353,13 @@ function parseVersionedPath(filePath) {
1353
1353
  function generateVersionedPath(filePath) {
1354
1354
  const parsed = parseVersionedPath(filePath);
1355
1355
  if (!parsed) {
1356
- let candidate$1 = `${filePath}-filled1`;
1357
- let version$1 = 1;
1358
- while (existsSync(candidate$1)) {
1359
- version$1++;
1360
- candidate$1 = `${filePath}-filled${version$1}`;
1356
+ let candidate = `${filePath}-filled1`;
1357
+ let version = 1;
1358
+ while (existsSync(candidate)) {
1359
+ version++;
1360
+ candidate = `${filePath}-filled${version}`;
1361
1361
  }
1362
- return candidate$1;
1362
+ return candidate;
1363
1363
  }
1364
1364
  let version = parsed.version !== null ? parsed.version + 1 : 1;
1365
1365
  let candidate = `${parsed.base}-filled${version}${parsed.extension}`;
@@ -1469,10 +1469,10 @@ async function promptForString(ctx) {
1469
1469
  placeholder: placeholderText,
1470
1470
  initialValue: currentVal ?? "",
1471
1471
  validate: (value) => {
1472
- if (field.required && !value.trim()) return "This field is required";
1473
- if (field.minLength && value.length < field.minLength) return `Minimum ${field.minLength} characters required`;
1474
- if (field.maxLength && value.length > field.maxLength) return `Maximum ${field.maxLength} characters allowed`;
1475
- if (field.pattern && !new RegExp(field.pattern).test(value)) return `Must match pattern: ${field.pattern}`;
1472
+ if (field.required && !value?.trim()) return "This field is required";
1473
+ if (field.minLength && (value?.length ?? 0) < field.minLength) return `Minimum ${field.minLength} characters required`;
1474
+ if (field.maxLength && (value?.length ?? 0) > field.maxLength) return `Maximum ${field.maxLength} characters allowed`;
1475
+ if (field.pattern && value && !new RegExp(field.pattern).test(value)) return `Must match pattern: ${field.pattern}`;
1476
1476
  }
1477
1477
  });
1478
1478
  if (p.isCancel(result)) return null;
@@ -1480,7 +1480,7 @@ async function promptForString(ctx) {
1480
1480
  return {
1481
1481
  op: "set_string",
1482
1482
  fieldId: field.id,
1483
- value: result || null
1483
+ value: result ?? null
1484
1484
  };
1485
1485
  }
1486
1486
  /**
@@ -1495,8 +1495,8 @@ async function promptForNumber(ctx) {
1495
1495
  placeholder: placeholderText,
1496
1496
  initialValue: currentVal !== null ? String(currentVal) : "",
1497
1497
  validate: (value) => {
1498
- if (field.required && !value.trim()) return "This field is required";
1499
- if (!value.trim()) return;
1498
+ if (field.required && !value?.trim()) return "This field is required";
1499
+ if (!value?.trim()) return;
1500
1500
  const num = Number(value);
1501
1501
  if (isNaN(num)) return "Please enter a valid number";
1502
1502
  if (field.integer && !Number.isInteger(num)) return "Please enter a whole number";
@@ -1527,14 +1527,14 @@ async function promptForStringList(ctx) {
1527
1527
  placeholder: hint,
1528
1528
  initialValue: currentItems.join("\n"),
1529
1529
  validate: (value) => {
1530
- const items$1 = value.split("\n").map((s) => s.trim()).filter(Boolean);
1531
- if (field.required && items$1.length === 0) return "At least one item is required";
1532
- if (field.minItems && items$1.length < field.minItems) return `Minimum ${field.minItems} items required`;
1533
- if (field.maxItems && items$1.length > field.maxItems) return `Maximum ${field.maxItems} items allowed`;
1530
+ const items = (value ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
1531
+ if (field.required && items.length === 0) return "At least one item is required";
1532
+ if (field.minItems && items.length < field.minItems) return `Minimum ${field.minItems} items required`;
1533
+ if (field.maxItems && items.length > field.maxItems) return `Maximum ${field.maxItems} items allowed`;
1534
1534
  }
1535
1535
  });
1536
1536
  if (p.isCancel(result)) return null;
1537
- const items = result.split("\n").map((s) => s.trim()).filter(Boolean);
1537
+ const items = (result ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
1538
1538
  if (items.length === 0 && !field.required) return null;
1539
1539
  return {
1540
1540
  op: "set_string_list",
@@ -1612,16 +1612,16 @@ async function promptForCheckboxes(ctx) {
1612
1612
  });
1613
1613
  if (p.isCancel(result)) return null;
1614
1614
  const selected = result;
1615
- const values$1 = {};
1616
- for (const opt of field.options) values$1[opt.id] = selected.includes(opt.id) ? "done" : "todo";
1615
+ const values = {};
1616
+ for (const opt of field.options) values[opt.id] = selected.includes(opt.id) ? "done" : "todo";
1617
1617
  return {
1618
1618
  op: "set_checkboxes",
1619
1619
  fieldId: field.id,
1620
- value: values$1
1620
+ value: values
1621
1621
  };
1622
1622
  }
1623
1623
  if (field.checkboxMode === "explicit") {
1624
- const values$1 = {};
1624
+ const values = {};
1625
1625
  for (const opt of field.options) {
1626
1626
  const current = currentValues[opt.id];
1627
1627
  const result = await p.select({
@@ -1643,12 +1643,12 @@ async function promptForCheckboxes(ctx) {
1643
1643
  initialValue: current === "yes" || current === "no" ? current : "unfilled"
1644
1644
  });
1645
1645
  if (p.isCancel(result)) return null;
1646
- values$1[opt.id] = result;
1646
+ values[opt.id] = result;
1647
1647
  }
1648
1648
  return {
1649
1649
  op: "set_checkboxes",
1650
1650
  fieldId: field.id,
1651
- value: values$1
1651
+ value: values
1652
1652
  };
1653
1653
  }
1654
1654
  const values = {};
@@ -1701,8 +1701,8 @@ async function promptForUrl(ctx) {
1701
1701
  placeholder: placeholderText,
1702
1702
  initialValue: currentVal ?? "",
1703
1703
  validate: (value) => {
1704
- if (field.required && !value.trim()) return "This field is required";
1705
- if (!value.trim()) return;
1704
+ if (field.required && !value?.trim()) return "This field is required";
1705
+ if (!value?.trim()) return;
1706
1706
  try {
1707
1707
  new URL(value);
1708
1708
  } catch {
@@ -1715,7 +1715,7 @@ async function promptForUrl(ctx) {
1715
1715
  return {
1716
1716
  op: "set_url",
1717
1717
  fieldId: field.id,
1718
- value: result || null
1718
+ value: result ?? null
1719
1719
  };
1720
1720
  }
1721
1721
  /**
@@ -1743,8 +1743,8 @@ async function promptForDate(ctx) {
1743
1743
  placeholder: currentVal ?? `YYYY-MM-DD${formatHint}`,
1744
1744
  initialValue: currentVal ?? "",
1745
1745
  validate: (value) => {
1746
- if (field.required && !value.trim()) return "This field is required";
1747
- if (!value.trim()) return;
1746
+ if (field.required && !value?.trim()) return "This field is required";
1747
+ if (!value?.trim()) return;
1748
1748
  if (!isValidDate(value)) return "Please enter a valid date in YYYY-MM-DD format";
1749
1749
  if (field.min && value < field.min) return `Date must be on or after ${field.min}`;
1750
1750
  if (field.max && value > field.max) return `Date must be on or before ${field.max}`;
@@ -1755,7 +1755,7 @@ async function promptForDate(ctx) {
1755
1755
  return {
1756
1756
  op: "set_date",
1757
1757
  fieldId: field.id,
1758
- value: result || null
1758
+ value: result ?? null
1759
1759
  };
1760
1760
  }
1761
1761
  /** Default year range for validation */
@@ -1774,8 +1774,8 @@ async function promptForYear(ctx) {
1774
1774
  placeholder: currentVal !== null ? String(currentVal) : `Year (${minYear}-${maxYear})`,
1775
1775
  initialValue: currentVal !== null ? String(currentVal) : "",
1776
1776
  validate: (value) => {
1777
- if (field.required && !value.trim()) return "This field is required";
1778
- if (!value.trim()) return;
1777
+ if (field.required && !value?.trim()) return "This field is required";
1778
+ if (!value?.trim()) return;
1779
1779
  const num = Number(value);
1780
1780
  if (isNaN(num) || !Number.isInteger(num)) return "Please enter a valid year (e.g., 2025)";
1781
1781
  if (num < minYear) return `Year must be ${minYear} or later`;
@@ -1805,11 +1805,11 @@ async function promptForUrlList(ctx) {
1805
1805
  placeholder: hint,
1806
1806
  initialValue: currentItems.join("\n"),
1807
1807
  validate: (value) => {
1808
- const items$1 = value.split("\n").map((s) => s.trim()).filter(Boolean);
1809
- if (field.required && items$1.length === 0) return "At least one URL is required";
1810
- if (field.minItems && items$1.length < field.minItems) return `Minimum ${field.minItems} URLs required`;
1811
- if (field.maxItems && items$1.length > field.maxItems) return `Maximum ${field.maxItems} URLs allowed`;
1812
- for (const item of items$1) try {
1808
+ const items = (value ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
1809
+ if (field.required && items.length === 0) return "At least one URL is required";
1810
+ if (field.minItems && items.length < field.minItems) return `Minimum ${field.minItems} URLs required`;
1811
+ if (field.maxItems && items.length > field.maxItems) return `Maximum ${field.maxItems} URLs allowed`;
1812
+ for (const item of items) try {
1813
1813
  new URL(item);
1814
1814
  } catch {
1815
1815
  return `Invalid URL: ${item}`;
@@ -1817,7 +1817,7 @@ async function promptForUrlList(ctx) {
1817
1817
  }
1818
1818
  });
1819
1819
  if (p.isCancel(result)) return null;
1820
- const items = result.split("\n").map((s) => s.trim()).filter(Boolean);
1820
+ const items = (result ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
1821
1821
  if (items.length === 0 && !field.required) return null;
1822
1822
  return {
1823
1823
  op: "set_url_list",
@@ -1947,7 +1947,7 @@ function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
1947
1947
  /**
1948
1948
  * Format a patch value for display with truncation.
1949
1949
  */
1950
- function formatPatchValue(patch) {
1950
+ function formatPatchValue$1(patch) {
1951
1951
  switch (patch.op) {
1952
1952
  case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
1953
1953
  case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
@@ -2052,7 +2052,7 @@ function createFillLoggingCallbacks(ctx, options = {}) {
2052
2052
  logInfo(ctx, ` -> ${pc.yellow(String(patches.length))} patch(es):`);
2053
2053
  for (const patch of patches) {
2054
2054
  const typeName = formatPatchType(patch);
2055
- const value = formatPatchValue(patch);
2055
+ const value = formatPatchValue$1(patch);
2056
2056
  const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2057
2057
  if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2058
2058
  else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
@@ -2188,7 +2188,7 @@ async function promptForModel(webSearchRequired) {
2188
2188
  message: "Model ID (provider/model-id):",
2189
2189
  placeholder: "anthropic/claude-sonnet-4-20250514",
2190
2190
  validate: (value) => {
2191
- if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
2191
+ if (!value?.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
2192
2192
  }
2193
2193
  });
2194
2194
  if (p.isCancel(customModel)) return null;
@@ -2278,6 +2278,7 @@ async function runAgentFillWorkflow(form, modelId, formsDir, filePath, isResearc
2278
2278
  fillMode: overwrite ? "overwrite" : "continue",
2279
2279
  enableWebSearch: isResearch,
2280
2280
  captureWireFormat: false,
2281
+ recordFill: false,
2281
2282
  callbacks
2282
2283
  });
2283
2284
  if (result.status.ok) p.log.success(pc.green(`Form completed in ${result.turns} turn(s)`));
@@ -2768,12 +2769,15 @@ function formatConsoleSession(transcript, useColors) {
2768
2769
  * Register the fill command.
2769
2770
  */
2770
2771
  function registerFillCommand(program) {
2771
- program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").option("--normalize", "Regenerate form without preserving external content").action(async (file, options, cmd) => {
2772
+ program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").option("--normalize", "Regenerate form without preserving external content").option("--record-fill", "Write fill record to sidecar .fill.json file").option("--record-fill-stable", "Write fill record without timestamps/durations (for golden tests)").action(async (file, options, cmd) => {
2772
2773
  const ctx = getCommandContext(cmd);
2773
2774
  const filePath = resolve(file);
2775
+ let harness;
2776
+ let collector;
2777
+ let targetRoles = [];
2778
+ let form;
2774
2779
  try {
2775
2780
  const startTime = Date.now();
2776
- let targetRoles;
2777
2781
  if (options.roles) try {
2778
2782
  targetRoles = parseRolesFlag(options.roles);
2779
2783
  } catch (error) {
@@ -2792,7 +2796,7 @@ function registerFillCommand(program) {
2792
2796
  logVerbose(ctx, `Reading form: ${filePath}`);
2793
2797
  const formContent = await readFile$1(filePath);
2794
2798
  logVerbose(ctx, "Parsing form...");
2795
- const form = parseForm(formContent);
2799
+ form = parseForm(formContent);
2796
2800
  if (options.interactive) {
2797
2801
  if (options.mock) {
2798
2802
  logError("--interactive cannot be used with --mock");
@@ -2817,19 +2821,19 @@ function registerFillCommand(program) {
2817
2821
  process.exit(1);
2818
2822
  }
2819
2823
  if (patches.length > 0) applyPatches(form, patches);
2820
- const durationMs$1 = Date.now() - startTime;
2821
- let outputPath$1;
2822
- if (options.output) outputPath$1 = resolve(options.output);
2824
+ const durationMs = Date.now() - startTime;
2825
+ let outputPath;
2826
+ if (options.output) outputPath = resolve(options.output);
2823
2827
  else {
2824
2828
  const formsDir = getFormsDir(ctx.formsDir);
2825
2829
  await ensureFormsDir(formsDir);
2826
- outputPath$1 = generateVersionedPathInFormsDir(filePath, formsDir);
2830
+ outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
2827
2831
  }
2828
2832
  if (ctx.dryRun) {
2829
- logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
2833
+ logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
2830
2834
  showInteractiveOutro(patches.length, false);
2831
2835
  } else {
2832
- const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(form, outputPath$1);
2836
+ const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(form, outputPath);
2833
2837
  showInteractiveOutro(patches.length, false);
2834
2838
  console.log("");
2835
2839
  p.log.success("Outputs:");
@@ -2838,11 +2842,11 @@ function registerFillCommand(program) {
2838
2842
  console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
2839
2843
  console.log(` ${formatPath(schemaPath)} ${pc.dim("(JSON Schema)")}`);
2840
2844
  }
2841
- logTiming(ctx, "Fill time", durationMs$1);
2845
+ logTiming(ctx, "Fill time", durationMs);
2842
2846
  if (patches.length > 0) {
2843
2847
  console.log("");
2844
2848
  console.log("Next step: fill remaining fields with agent");
2845
- console.log(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`);
2849
+ console.log(` markform fill ${formatPath(outputPath)} --model=<provider/model>`);
2846
2850
  }
2847
2851
  process.exit(0);
2848
2852
  }
@@ -2857,7 +2861,7 @@ function registerFillCommand(program) {
2857
2861
  process.exit(1);
2858
2862
  }
2859
2863
  if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
2860
- const harnessConfig = resolveHarnessConfig(form, {
2864
+ const cliOptions = {
2861
2865
  maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
2862
2866
  maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
2863
2867
  maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
@@ -2865,8 +2869,9 @@ function registerFillCommand(program) {
2865
2869
  maxGroupsPerTurn: options.maxGroups ? parseInt(options.maxGroups, 10) : void 0,
2866
2870
  targetRoles,
2867
2871
  fillMode
2868
- });
2869
- const harness = createHarness(form, harnessConfig);
2872
+ };
2873
+ const harnessConfig = resolveHarnessConfig(form, cliOptions);
2874
+ harness = createHarness(form, harnessConfig);
2870
2875
  let agent;
2871
2876
  let mockPath;
2872
2877
  let agentProvider;
@@ -2877,12 +2882,36 @@ function registerFillCommand(program) {
2877
2882
  mockPath = resolve(options.mockSource);
2878
2883
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
2879
2884
  agent = createMockAgent(parseForm(await readFile$1(mockPath)));
2885
+ const structureSummary = computeStructureSummary(form.schema);
2886
+ collector = new FillRecordCollector({
2887
+ form: {
2888
+ id: form.schema.id,
2889
+ title: form.schema.title,
2890
+ description: form.schema.description,
2891
+ structure: structureSummary
2892
+ },
2893
+ provider: "mock",
2894
+ model: "mock",
2895
+ parallelEnabled: false
2896
+ });
2880
2897
  } else {
2881
2898
  const modelIdString = options.model;
2882
2899
  logVerbose(ctx, `Resolving model: ${modelIdString}`);
2883
2900
  const { model, provider, modelId } = await resolveModel(modelIdString);
2884
2901
  agentProvider = provider;
2885
2902
  agentModelName = modelId;
2903
+ const structureSummary = computeStructureSummary(form.schema);
2904
+ collector = new FillRecordCollector({
2905
+ form: {
2906
+ id: form.schema.id,
2907
+ title: form.schema.title,
2908
+ description: form.schema.description,
2909
+ structure: structureSummary
2910
+ },
2911
+ provider,
2912
+ model: modelIdString,
2913
+ parallelEnabled: false
2914
+ });
2886
2915
  let systemPrompt;
2887
2916
  if (options.instructions) {
2888
2917
  systemPrompt = options.instructions;
@@ -2892,13 +2921,44 @@ function registerFillCommand(program) {
2892
2921
  logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
2893
2922
  systemPrompt = await readFile$1(promptPath);
2894
2923
  }
2895
- const callbacks = createCliToolCallbacks({
2924
+ const cliCallbacks = createCliToolCallbacks({
2896
2925
  message: (msg) => currentSpinner?.message(msg),
2897
2926
  update: (context) => currentSpinner?.update(context),
2898
2927
  stop: (msg) => currentSpinner?.stop(msg),
2899
2928
  error: (msg) => currentSpinner?.error(msg),
2900
2929
  getElapsedMs: () => currentSpinner?.getElapsedMs() ?? 0
2901
2930
  }, ctx);
2931
+ const liveCollector = collector;
2932
+ const callbacks = {
2933
+ onTurnStart: (turn) => {
2934
+ liveCollector.onTurnStart({
2935
+ turnNumber: turn.turnNumber,
2936
+ issuesCount: turn.issuesCount,
2937
+ order: turn.order ?? 0,
2938
+ executionId: turn.executionId ?? "cli-serial"
2939
+ });
2940
+ },
2941
+ onTurnComplete: (progress) => {
2942
+ liveCollector.onTurnComplete(progress);
2943
+ },
2944
+ onToolStart: (call) => {
2945
+ cliCallbacks.onToolStart?.(call);
2946
+ liveCollector.onToolStart(call);
2947
+ },
2948
+ onToolEnd: (call) => {
2949
+ cliCallbacks.onToolEnd?.(call);
2950
+ liveCollector.onToolEnd(call);
2951
+ },
2952
+ onLlmCallStart: (call) => {
2953
+ liveCollector.onLlmCallStart(call);
2954
+ },
2955
+ onLlmCallEnd: (call) => {
2956
+ liveCollector.onLlmCallEnd(call);
2957
+ },
2958
+ onWebSearch: (info) => {
2959
+ liveCollector.onWebSearch(info);
2960
+ }
2961
+ };
2902
2962
  targetRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0] ?? AGENT_ROLE;
2903
2963
  const liveAgent = createLiveAgent({
2904
2964
  model,
@@ -2922,6 +2982,12 @@ function registerFillCommand(program) {
2922
2982
  let stepResult = harness.step();
2923
2983
  let previousRejections;
2924
2984
  logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2985
+ collector.onTurnStart({
2986
+ turnNumber: stepResult.turnNumber,
2987
+ issuesCount: stepResult.issues.length,
2988
+ order: 0,
2989
+ executionId: "cli-serial"
2990
+ });
2925
2991
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2926
2992
  let spinner = null;
2927
2993
  if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet) {
@@ -2948,7 +3014,7 @@ function registerFillCommand(program) {
2948
3014
  logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
2949
3015
  for (const patch of patches) {
2950
3016
  const typeName = formatPatchType(patch);
2951
- const value = formatPatchValue(patch);
3017
+ const value = formatPatchValue$1(patch);
2952
3018
  const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2953
3019
  if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2954
3020
  else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
@@ -2987,12 +3053,31 @@ function registerFillCommand(program) {
2987
3053
  contextPrompt: wire.request.prompt
2988
3054
  };
2989
3055
  }
3056
+ const prevTurnNumber = stepResult.turnNumber;
3057
+ const prevIssuesShown = stepResult.issues.length;
2990
3058
  stepResult = harness.apply(patches, stepResult.issues, llmStats, context, wire);
3059
+ const rejectedPatches = stepResult.rejectedPatches ?? [];
3060
+ collector.onTurnComplete({
3061
+ turnNumber: prevTurnNumber,
3062
+ issuesShown: prevIssuesShown,
3063
+ patchesApplied: patches.length - rejectedPatches.length,
3064
+ requiredIssuesRemaining: stepResult.issues.filter((i) => i.severity === "required").length,
3065
+ isComplete: stepResult.isComplete,
3066
+ rejectedPatches,
3067
+ issues: stepResult.issues,
3068
+ patches
3069
+ });
2991
3070
  previousRejections = stepResult.rejectedPatches;
2992
3071
  if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
2993
3072
  else if (!harness.hasReachedMaxTurns()) {
2994
3073
  stepResult = harness.step();
2995
3074
  logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
3075
+ collector.onTurnStart({
3076
+ turnNumber: stepResult.turnNumber,
3077
+ issuesCount: stepResult.issues.length,
3078
+ order: 0,
3079
+ executionId: "cli-serial"
3080
+ });
2996
3081
  }
2997
3082
  }
2998
3083
  const durationMs = Date.now() - startTime;
@@ -3012,6 +3097,24 @@ function registerFillCommand(program) {
3012
3097
  await writeFile(outputPath, formMarkdown);
3013
3098
  logSuccess(ctx, `Form written to: ${outputPath}`);
3014
3099
  }
3100
+ const finalInspect = inspect(harness.getForm(), { targetRoles });
3101
+ const progressSummary = computeProgressSummary(form.schema, harness.getForm().responsesByFieldId, harness.getForm().notes, finalInspect.issues);
3102
+ collector.setStatus(stepResult.isComplete ? "completed" : "partial", stepResult.isComplete ? void 0 : "max_turns");
3103
+ const fillRecord = collector.getRecord(progressSummary.counts);
3104
+ if (!ctx.quiet) {
3105
+ console.log("");
3106
+ const summary = formatFillRecordSummary(fillRecord, { verbose: ctx.verbose });
3107
+ console.error(summary);
3108
+ }
3109
+ if (options.recordFill || options.recordFillStable) {
3110
+ const sidecarPath = deriveFillRecordPath(outputPath);
3111
+ const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(fillRecord) : fillRecord;
3112
+ if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write fill record to: ${sidecarPath}`);
3113
+ else {
3114
+ writeFileSync(sidecarPath, JSON.stringify(recordToWrite, null, 2));
3115
+ logSuccess(ctx, `Fill record written to: ${sidecarPath}`);
3116
+ }
3117
+ }
3015
3118
  const transcript = buildSessionTranscript(filePath, options.mock ? "mock" : "live", mockPath, options.model, harnessConfig, harness.getTurns(), stepResult.isComplete, outputPath);
3016
3119
  if (options.record) {
3017
3120
  const recordPath = resolve(options.record);
@@ -3023,13 +3126,25 @@ function registerFillCommand(program) {
3023
3126
  await writeFile(recordPath, yaml);
3024
3127
  logSuccess(ctx, `Session recorded to: ${recordPath}`);
3025
3128
  }
3026
- } else {
3129
+ } else if (!ctx.quiet) {
3027
3130
  const output = formatOutput(ctx, transcript, (data, useColors) => formatConsoleSession(data, useColors));
3028
3131
  console.log(output);
3029
3132
  }
3030
3133
  process.exit(stepResult.isComplete ? 0 : 1);
3031
3134
  } catch (error) {
3032
- logError(error instanceof Error ? error.message : String(error));
3135
+ const message = error instanceof Error ? error.message : String(error);
3136
+ logError(message);
3137
+ if ((options.recordFill || options.recordFillStable) && collector && harness && form && options.output) try {
3138
+ const currentForm = harness.getForm();
3139
+ const finalInspect = inspect(currentForm, { targetRoles });
3140
+ const progressSummary = computeProgressSummary(form.schema, currentForm.responsesByFieldId, currentForm.notes, finalInspect.issues);
3141
+ collector.setStatus("failed", message);
3142
+ const fillRecord = collector.getRecord(progressSummary.counts);
3143
+ const sidecarPath = deriveFillRecordPath(resolve(options.output));
3144
+ const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(fillRecord) : fillRecord;
3145
+ writeFileSync(sidecarPath, JSON.stringify(recordToWrite, null, 2));
3146
+ logWarn(ctx, `Partial fill record written to: ${sidecarPath}`);
3147
+ } catch {}
3033
3148
  process.exit(1);
3034
3149
  }
3035
3150
  });
@@ -3098,7 +3213,6 @@ function formatPriority$1(priority, useColors) {
3098
3213
  case 2: return pc.yellow(label);
3099
3214
  case 3: return pc.cyan(label);
3100
3215
  case 4: return pc.blue(label);
3101
- case 5:
3102
3216
  default: return pc.dim(label);
3103
3217
  }
3104
3218
  }
@@ -3595,10 +3709,10 @@ function buildPlanItem(form, item) {
3595
3709
  }
3596
3710
  function formatConsolePlan(report, useColors) {
3597
3711
  const lines = [];
3598
- const bold = useColors ? pc.bold : (s$1) => s$1;
3599
- const dim = useColors ? pc.dim : (s$1) => s$1;
3600
- const cyan = useColors ? pc.cyan : (s$1) => s$1;
3601
- const yellow = useColors ? pc.yellow : (s$1) => s$1;
3712
+ const bold = useColors ? pc.bold : (s) => s;
3713
+ const dim = useColors ? pc.dim : (s) => s;
3714
+ const cyan = useColors ? pc.cyan : (s) => s;
3715
+ const yellow = useColors ? pc.yellow : (s) => s;
3602
3716
  const titlePart = report.title ? ` (${report.title})` : "";
3603
3717
  lines.push(bold(cyan(`Plan: ${report.formId}${titlePart}`)));
3604
3718
  lines.push("");
@@ -3738,10 +3852,10 @@ function openBrowser(url) {
3738
3852
  /**
3739
3853
  * Build tabs for a form file.
3740
3854
  * All tabs are always present - content is generated dynamically from the form.
3741
- * Tab order: View, Edit, Source, Report, Values, Schema
3855
+ * Tab order: View, Edit, Source, Report, Values, Schema, Fill Record (if sidecar exists)
3742
3856
  */
3743
3857
  function buildFormTabs(formPath) {
3744
- return [
3858
+ const tabs = [
3745
3859
  {
3746
3860
  id: "view",
3747
3861
  label: "View",
@@ -3773,6 +3887,13 @@ function buildFormTabs(formPath) {
3773
3887
  path: null
3774
3888
  }
3775
3889
  ];
3890
+ const sidecarPath = deriveFillRecordPath(formPath);
3891
+ if (existsSync(sidecarPath)) tabs.push({
3892
+ id: "fill-record",
3893
+ label: "Fill Record",
3894
+ path: sidecarPath
3895
+ });
3896
+ return tabs;
3776
3897
  }
3777
3898
  /**
3778
3899
  * Register the serve command.
@@ -3862,6 +3983,18 @@ async function handleRequest(req, res, filePath, fileType, form, ctx, tabs, upda
3862
3983
  res.end(html);
3863
3984
  return;
3864
3985
  }
3986
+ if (tabId === "fill-record" && tab?.path) try {
3987
+ const content = await readFile(tab.path, "utf-8");
3988
+ const html = renderFillRecordContent(JSON.parse(content));
3989
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3990
+ res.end(html);
3991
+ return;
3992
+ } catch (error) {
3993
+ const message = error instanceof Error ? error.message : "Unknown error";
3994
+ res.writeHead(500, { "Content-Type": "text/plain" });
3995
+ res.end(`Error loading fill record: ${message}`);
3996
+ return;
3997
+ }
3865
3998
  res.writeHead(404);
3866
3999
  res.end("Tab not found");
3867
4000
  return;
@@ -4543,6 +4676,34 @@ function renderFormHtml(form, tabs) {
4543
4676
  </div>
4544
4677
  ${showTabs ? "<div id=\"tab-other\" class=\"tab-content\"><div class=\"loading\">Loading...</div></div>" : ""}
4545
4678
  <script>
4679
+ // Copy YAML content handler for Fill Record tab (must be global for dynamically loaded content)
4680
+ function frCopyYaml(btn) {
4681
+ const pre = btn.parentElement.querySelector('pre');
4682
+ navigator.clipboard.writeText(pre.textContent).then(() => {
4683
+ const orig = btn.textContent;
4684
+ btn.textContent = 'Copied!';
4685
+ setTimeout(() => btn.textContent = orig, 1500);
4686
+ });
4687
+ }
4688
+
4689
+ // Tooltip handlers for Fill Record visualizations (must be global for dynamically loaded content)
4690
+ function frShowTip(el) {
4691
+ var tip = document.getElementById('fr-tooltip');
4692
+ if (tip && el.dataset.tooltip) {
4693
+ tip.textContent = el.dataset.tooltip;
4694
+ // Position tooltip centered above the element
4695
+ var rect = el.getBoundingClientRect();
4696
+ tip.style.left = (rect.left + rect.width / 2) + 'px';
4697
+ tip.style.top = (rect.top - 8) + 'px';
4698
+ tip.style.transform = 'translate(-50%, -100%)';
4699
+ tip.classList.add('visible');
4700
+ }
4701
+ }
4702
+ function frHideTip() {
4703
+ var tip = document.getElementById('fr-tooltip');
4704
+ if (tip) tip.classList.remove('visible');
4705
+ }
4706
+
4546
4707
  // Track fields marked for skip
4547
4708
  const skippedFields = new Set();
4548
4709
 
@@ -5315,7 +5476,7 @@ function renderViewFieldValue(field, value, isSkipped) {
5315
5476
  case "string": {
5316
5477
  const v = value.kind === "string" ? value.value : null;
5317
5478
  if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5318
- return `<div class="view-field-value">${escapeHtml(v)}</div>`;
5479
+ return `<div class="view-field-value">${formatBareUrlsAsHtmlLinks(v, escapeHtml)}</div>`;
5319
5480
  }
5320
5481
  case "number": {
5321
5482
  const v = value.kind === "number" ? value.value : null;
@@ -5325,7 +5486,7 @@ function renderViewFieldValue(field, value, isSkipped) {
5325
5486
  case "string_list": {
5326
5487
  const items = value.kind === "string_list" ? value.items : [];
5327
5488
  if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5328
- return `<div class="view-field-value"><ul>${items.map((i) => `<li>${escapeHtml(i)}</li>`).join("")}</ul></div>`;
5489
+ return `<div class="view-field-value"><ul>${items.map((i) => `<li>${formatBareUrlsAsHtmlLinks(i, escapeHtml)}</li>`).join("")}</ul></div>`;
5329
5490
  }
5330
5491
  case "single_select": {
5331
5492
  const selected = value.kind === "single_select" ? value.selected : null;
@@ -5389,7 +5550,7 @@ function renderViewFieldValue(field, value, isSkipped) {
5389
5550
  if (col.type === "url" && cellValue) {
5390
5551
  const domain = friendlyUrlAbbrev(cellValue);
5391
5552
  cellHtml = `<a href="${escapeHtml(cellValue)}" target="_blank" class="url-link" data-url="${escapeHtml(cellValue)}">${escapeHtml(domain)}</a>`;
5392
- } else cellHtml = escapeHtml(cellValue);
5553
+ } else cellHtml = formatBareUrlsAsHtmlLinks(cellValue, escapeHtml);
5393
5554
  }
5394
5555
  tableHtml += `<td>${cellHtml}</td>`;
5395
5556
  }
@@ -5600,6 +5761,7 @@ function renderMarkdownContent(content) {
5600
5761
  }
5601
5762
  /**
5602
5763
  * Format inline markdown (bold, italic, code, links, checkboxes).
5764
+ * Also auto-links bare URLs for consistency.
5603
5765
  */
5604
5766
  function formatInlineMarkdown(text) {
5605
5767
  let result = escapeHtml(text);
@@ -5612,6 +5774,12 @@ function formatInlineMarkdown(text) {
5612
5774
  const cleanUrl = url.replace(/&amp;/g, "&");
5613
5775
  return `<a href="${cleanUrl}" target="_blank" class="url-link" data-url="${cleanUrl}">${linkText}</a>`;
5614
5776
  });
5777
+ result = result.replace(/(?<!href="|data-url="|">|\]\()(?:https?:\/\/|www\.)[^\s<>"]+(?<![.,;:!?'")])/g, (url) => {
5778
+ const cleanUrl = url.replace(/&amp;/g, "&");
5779
+ const fullUrl = cleanUrl.startsWith("www.") ? `https://${cleanUrl}` : cleanUrl;
5780
+ const display = friendlyUrlAbbrev(fullUrl);
5781
+ return `<a href="${escapeHtml(fullUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(fullUrl)}">${escapeHtml(display)}</a>`;
5782
+ });
5615
5783
  return result;
5616
5784
  }
5617
5785
  /**
@@ -5656,6 +5824,789 @@ function renderJsonContent(content) {
5656
5824
  }
5657
5825
  return `<pre>${formatted.replace(/"([^"]+)":/g, "<span class=\"syn-key\">\"$1\"</span>:").replace(/: "([^"]*)"/g, ": <span class=\"syn-string\">\"$1\"</span>").replace(/: (-?\d+\.?\d*)/g, ": <span class=\"syn-number\">$1</span>").replace(/: (true|false)/g, ": <span class=\"syn-bool\">$1</span>").replace(/: (null)/g, ": <span class=\"syn-null\">$1</span>")}</pre>`;
5658
5826
  }
5827
+ /**
5828
+ * Format milliseconds as human-readable duration.
5829
+ * @public Exported for reuse in other visualizations.
5830
+ */
5831
+ function formatDuration(ms) {
5832
+ if (ms < 1e3) return `${ms.toFixed(0)}ms`;
5833
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
5834
+ return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(0)}s`;
5835
+ }
5836
+ /**
5837
+ * Format token count with K suffix for large numbers.
5838
+ * @public Exported for reuse in other visualizations.
5839
+ */
5840
+ function formatTokens(count) {
5841
+ if (count >= 1e4) return `${(count / 1e3).toFixed(1)}k`;
5842
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
5843
+ return count.toLocaleString();
5844
+ }
5845
+ /**
5846
+ * Format a patch value for display.
5847
+ * Shows full content - the container has max-height with scroll for long values.
5848
+ */
5849
+ function formatPatchValue(value) {
5850
+ if (value === null || value === void 0) return "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
5851
+ if (typeof value === "string") return escapeHtml(value);
5852
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
5853
+ return escapeHtml(JSON.stringify(value, null, 2));
5854
+ }
5855
+ /**
5856
+ * Render patches from a fill_form tool call input.
5857
+ * Returns HTML for the patch details section.
5858
+ */
5859
+ function renderPatchDetails(input) {
5860
+ const patches = input.patches;
5861
+ if (!Array.isArray(patches) || patches.length === 0) return "";
5862
+ return `<div class="fr-turn__patches">${patches.map((patch) => {
5863
+ if (!patch || typeof patch !== "object") return "";
5864
+ const p = patch;
5865
+ const op = typeof p.op === "string" ? p.op : "unknown";
5866
+ const fieldId = typeof p.fieldId === "string" ? p.fieldId : typeof p.noteId === "string" ? p.noteId : "";
5867
+ const opLabel = op.replace(/_/g, " ");
5868
+ let valueHtml = "";
5869
+ if (op === "skip_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(skipped)</em>";
5870
+ else if (op === "abort_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(aborted)</em>";
5871
+ else if (op === "clear_field") valueHtml = "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
5872
+ else if ("value" in p) valueHtml = formatPatchValue(p.value);
5873
+ else if ("values" in p) valueHtml = formatPatchValue(p.values);
5874
+ else if ("rows" in p) valueHtml = formatPatchValue(p.rows);
5875
+ return `
5876
+ <div class="fr-turn__patch">
5877
+ <span class="fr-turn__patch-field">${escapeHtml(fieldId)}</span>
5878
+ <span class="fr-turn__patch-op">${escapeHtml(opLabel)}</span>
5879
+ <span class="fr-turn__patch-value">${valueHtml}</span>
5880
+ </div>
5881
+ `;
5882
+ }).filter(Boolean).join("")}</div>`;
5883
+ }
5884
+ /**
5885
+ * Render a single tool call with enhanced details.
5886
+ * Shows query for web_search, patch details for fill_form.
5887
+ */
5888
+ function renderToolCall(tc) {
5889
+ const hasError = !!tc.result?.error;
5890
+ const icon = tc.success ? "✓" : "✕";
5891
+ const errorClass = hasError ? " fr-turn__tool--error" : "";
5892
+ let resultSummary = "";
5893
+ if (hasError) resultSummary = `Error: ${escapeHtml(tc.result?.error ?? "")}`;
5894
+ else if (tc.result?.resultCount !== void 0) resultSummary = `${tc.result.resultCount} results`;
5895
+ else resultSummary = "OK";
5896
+ let detailHtml = "";
5897
+ if (tc.tool === "web_search" && typeof tc.input.query === "string") detailHtml = ` <span class="fr-turn__query">"${escapeHtml(tc.input.query)}"</span>`;
5898
+ const toolLine = `<li class="fr-turn__tool${errorClass}">${icon} <strong>${escapeHtml(tc.tool)}</strong>${detailHtml}: ${resultSummary} (${formatDuration(tc.durationMs)})</li>`;
5899
+ if (tc.tool === "fill_form" && tc.input.patches) {
5900
+ const patchDetails = renderPatchDetails(tc.input);
5901
+ if (patchDetails) return toolLine + patchDetails;
5902
+ }
5903
+ return toolLine;
5904
+ }
5905
+ /**
5906
+ * CSS styles for fill record visualization.
5907
+ * Uses CSS custom properties for theming (supports dark mode via prefers-color-scheme).
5908
+ * Designed to be lightweight, reusable, and embeddable.
5909
+ */
5910
+ const FILL_RECORD_STYLES = `
5911
+ <style>
5912
+ .fr-dashboard {
5913
+ --fr-bg: #ffffff;
5914
+ --fr-bg-muted: #f9fafb;
5915
+ --fr-bg-subtle: #f3f4f6;
5916
+ --fr-border: #e5e7eb;
5917
+ --fr-text: #111827;
5918
+ --fr-text-muted: #6b7280;
5919
+ --fr-primary: #3b82f6;
5920
+ --fr-success: #22c55e;
5921
+ --fr-warning: #f59e0b;
5922
+ --fr-error: #ef4444;
5923
+ --fr-info: #6b7280;
5924
+
5925
+ /* Typography - consolidated to fewer sizes */
5926
+ --fr-font-sm: 13px;
5927
+ --fr-font-base: 14px;
5928
+ --fr-font-lg: 20px;
5929
+
5930
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
5931
+ padding: 20px;
5932
+ max-width: 900px;
5933
+ margin: 0 auto;
5934
+ color: var(--fr-text);
5935
+ line-height: 1.5;
5936
+ }
5937
+
5938
+ @media (prefers-color-scheme: dark) {
5939
+ .fr-dashboard {
5940
+ --fr-bg: #1f2937;
5941
+ --fr-bg-muted: #374151;
5942
+ --fr-bg-subtle: #4b5563;
5943
+ --fr-border: #4b5563;
5944
+ --fr-text: #f9fafb;
5945
+ --fr-text-muted: #9ca3af;
5946
+ }
5947
+ }
5948
+
5949
+ .fr-header {
5950
+ display: flex;
5951
+ justify-content: space-between;
5952
+ align-items: center;
5953
+ margin-bottom: 16px;
5954
+ padding-bottom: 12px;
5955
+ border-bottom: 1px solid var(--fr-border);
5956
+ }
5957
+ .fr-header__model {
5958
+ font-weight: 600;
5959
+ font-size: var(--fr-font-base);
5960
+ color: var(--fr-text);
5961
+ }
5962
+ .fr-header__time {
5963
+ font-weight: 600;
5964
+ font-size: var(--fr-font-base);
5965
+ color: var(--fr-text);
5966
+ }
5967
+
5968
+ .fr-banner {
5969
+ border-radius: 8px;
5970
+ padding: 12px 16px;
5971
+ margin-bottom: 20px;
5972
+ font-size: var(--fr-font-base);
5973
+ }
5974
+ .fr-banner--error {
5975
+ background: color-mix(in srgb, var(--fr-error) 10%, var(--fr-bg));
5976
+ border: 1px solid var(--fr-error);
5977
+ }
5978
+ .fr-banner--warning {
5979
+ background: color-mix(in srgb, var(--fr-warning) 10%, var(--fr-bg));
5980
+ border: 1px solid var(--fr-warning);
5981
+ }
5982
+
5983
+ .fr-cards {
5984
+ display: grid;
5985
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
5986
+ gap: 16px;
5987
+ margin-bottom: 24px;
5988
+ }
5989
+
5990
+ .fr-card {
5991
+ padding: 16px;
5992
+ background: var(--fr-bg-muted);
5993
+ border-radius: 8px;
5994
+ text-align: center;
5995
+ }
5996
+ .fr-card__label {
5997
+ font-size: var(--fr-font-sm);
5998
+ color: var(--fr-text-muted);
5999
+ margin-bottom: 4px;
6000
+ }
6001
+ .fr-card__value {
6002
+ font-size: var(--fr-font-lg);
6003
+ font-weight: 600;
6004
+ }
6005
+ .fr-card__sub {
6006
+ font-size: var(--fr-font-sm);
6007
+ color: var(--fr-text-muted);
6008
+ margin-top: 2px;
6009
+ }
6010
+
6011
+ .fr-badge {
6012
+ display: inline-flex;
6013
+ align-items: center;
6014
+ gap: 4px;
6015
+ padding: 4px 10px;
6016
+ border-radius: 4px;
6017
+ font-weight: 600;
6018
+ font-size: var(--fr-font-sm);
6019
+ }
6020
+ .fr-badge--completed { background: color-mix(in srgb, var(--fr-success) 15%, transparent); color: var(--fr-success); }
6021
+ .fr-badge--partial { background: color-mix(in srgb, var(--fr-warning) 15%, transparent); color: var(--fr-warning); }
6022
+ .fr-badge--cancelled { background: color-mix(in srgb, var(--fr-info) 15%, transparent); color: var(--fr-info); }
6023
+ .fr-badge--failed { background: color-mix(in srgb, var(--fr-error) 15%, transparent); color: var(--fr-error); }
6024
+
6025
+ .fr-section {
6026
+ margin-bottom: 24px;
6027
+ }
6028
+ .fr-section__title {
6029
+ font-size: var(--fr-font-base);
6030
+ font-weight: 500;
6031
+ color: var(--fr-text);
6032
+ margin-bottom: 8px;
6033
+ }
6034
+
6035
+ .fr-progress {
6036
+ background: var(--fr-border);
6037
+ border-radius: 4px;
6038
+ height: 20px;
6039
+ overflow: hidden;
6040
+ }
6041
+ .fr-progress__bar {
6042
+ background: var(--fr-primary);
6043
+ height: 100%;
6044
+ transition: width 0.3s ease;
6045
+ }
6046
+ .fr-progress__text {
6047
+ font-size: var(--fr-font-sm);
6048
+ color: var(--fr-text-muted);
6049
+ margin-top: 4px;
6050
+ }
6051
+
6052
+ .fr-progress__segments {
6053
+ display: flex;
6054
+ height: 100%;
6055
+ width: 100%;
6056
+ }
6057
+ .fr-progress-segment {
6058
+ height: 100%;
6059
+ min-width: 2px;
6060
+ border-right: 2px solid var(--fr-bg);
6061
+ cursor: pointer;
6062
+ }
6063
+ .fr-progress-segment:last-child {
6064
+ border-right: none;
6065
+ }
6066
+ .fr-progress-segment--filled {
6067
+ background: var(--fr-primary);
6068
+ }
6069
+ .fr-progress-segment--filled:hover {
6070
+ background: color-mix(in srgb, var(--fr-primary) 70%, white);
6071
+ }
6072
+ .fr-progress-segment--prefilled {
6073
+ background: #8b5cf6;
6074
+ }
6075
+ .fr-progress-segment--prefilled:hover {
6076
+ background: color-mix(in srgb, #8b5cf6 70%, white);
6077
+ }
6078
+ .fr-progress-segment--skipped {
6079
+ background: var(--fr-warning);
6080
+ }
6081
+ .fr-progress-segment--skipped:hover {
6082
+ background: color-mix(in srgb, var(--fr-warning) 70%, white);
6083
+ }
6084
+ .fr-progress-segment--empty {
6085
+ background: var(--fr-border);
6086
+ }
6087
+
6088
+ /* Gantt chart - each call on its own row */
6089
+ .fr-gantt {
6090
+ margin-bottom: 8px;
6091
+ }
6092
+ .fr-gantt__row {
6093
+ display: flex;
6094
+ align-items: center;
6095
+ height: 20px;
6096
+ margin-bottom: 3px;
6097
+ }
6098
+ .fr-gantt__label {
6099
+ width: 90px;
6100
+ flex-shrink: 0;
6101
+ font-size: 11px;
6102
+ color: var(--fr-text-muted);
6103
+ white-space: nowrap;
6104
+ overflow: hidden;
6105
+ text-overflow: ellipsis;
6106
+ padding-right: 8px;
6107
+ text-align: right;
6108
+ }
6109
+ .fr-gantt__track {
6110
+ flex: 1;
6111
+ background: var(--fr-bg-subtle);
6112
+ border-radius: 3px;
6113
+ height: 14px;
6114
+ position: relative;
6115
+ }
6116
+ .fr-gantt__bar {
6117
+ position: absolute;
6118
+ top: 2px;
6119
+ height: calc(100% - 4px);
6120
+ min-width: 6px;
6121
+ border-radius: 2px;
6122
+ cursor: pointer;
6123
+ }
6124
+ .fr-gantt__bar:hover {
6125
+ filter: brightness(1.15);
6126
+ }
6127
+ .fr-gantt__bar--llm {
6128
+ background: var(--fr-primary);
6129
+ }
6130
+ .fr-gantt__bar--tool {
6131
+ background: var(--fr-success);
6132
+ }
6133
+ .fr-gantt__legend {
6134
+ display: flex;
6135
+ gap: 16px;
6136
+ font-size: var(--fr-font-sm);
6137
+ color: var(--fr-text-muted);
6138
+ margin-top: 12px;
6139
+ padding-top: 8px;
6140
+ border-top: 1px solid var(--fr-border);
6141
+ }
6142
+ .fr-gantt__legend-item {
6143
+ display: flex;
6144
+ align-items: center;
6145
+ gap: 6px;
6146
+ }
6147
+ .fr-gantt__legend-dot {
6148
+ width: 10px;
6149
+ height: 10px;
6150
+ border-radius: 2px;
6151
+ }
6152
+ .fr-gantt__legend-dot--llm { background: var(--fr-primary); }
6153
+ .fr-gantt__legend-dot--tool { background: var(--fr-success); }
6154
+
6155
+ /* Tooltip container */
6156
+ .fr-tooltip {
6157
+ position: fixed;
6158
+ background: #1f2937;
6159
+ color: #f9fafb;
6160
+ padding: 8px 12px;
6161
+ border-radius: 4px;
6162
+ font-size: var(--fr-font-sm);
6163
+ white-space: pre-line;
6164
+ pointer-events: none;
6165
+ z-index: 1000;
6166
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
6167
+ opacity: 0;
6168
+ visibility: hidden;
6169
+ transition: opacity 0.05s ease-out, visibility 0.05s ease-out;
6170
+ }
6171
+ .fr-tooltip.visible {
6172
+ opacity: 1;
6173
+ visibility: visible;
6174
+ transition: opacity 0.2s ease-in, visibility 0.2s ease-in;
6175
+ }
6176
+
6177
+ .fr-table {
6178
+ width: 100%;
6179
+ border-collapse: collapse;
6180
+ font-size: var(--fr-font-sm);
6181
+ }
6182
+ .fr-table th {
6183
+ padding: 8px 12px;
6184
+ text-align: left;
6185
+ font-weight: 600;
6186
+ background: var(--fr-bg-subtle);
6187
+ }
6188
+ .fr-table th:not(:first-child) { text-align: center; }
6189
+ .fr-table td {
6190
+ padding: 8px 12px;
6191
+ border-bottom: 1px solid var(--fr-border);
6192
+ }
6193
+ .fr-table td:not(:first-child) { text-align: center; }
6194
+
6195
+ .fr-details {
6196
+ border: none;
6197
+ background: none;
6198
+ }
6199
+ .fr-details > summary {
6200
+ cursor: pointer;
6201
+ font-size: var(--fr-font-base);
6202
+ font-weight: 500;
6203
+ color: var(--fr-text);
6204
+ padding: 8px 0;
6205
+ list-style: none;
6206
+ }
6207
+ .fr-details > summary::-webkit-details-marker { display: none; }
6208
+ .fr-details > summary::before {
6209
+ content: '▶';
6210
+ display: inline-block;
6211
+ margin-right: 8px;
6212
+ transition: transform 0.2s;
6213
+ font-size: 11px;
6214
+ }
6215
+ .fr-details[open] > summary::before {
6216
+ transform: rotate(90deg);
6217
+ }
6218
+ .fr-details__content {
6219
+ background: var(--fr-bg-muted);
6220
+ border-radius: 8px;
6221
+ padding: 16px;
6222
+ margin-top: 8px;
6223
+ }
6224
+
6225
+ .fr-turn {
6226
+ margin-bottom: 8px;
6227
+ background: var(--fr-bg-muted);
6228
+ border-radius: 4px;
6229
+ }
6230
+ .fr-turn summary {
6231
+ cursor: pointer;
6232
+ padding: 12px;
6233
+ font-size: var(--fr-font-sm);
6234
+ list-style: none;
6235
+ }
6236
+ .fr-turn summary::-webkit-details-marker { display: none; }
6237
+ .fr-turn summary::before {
6238
+ content: '▶';
6239
+ display: inline-block;
6240
+ margin-right: 8px;
6241
+ transition: transform 0.2s;
6242
+ font-size: 11px;
6243
+ }
6244
+ .fr-turn[open] summary::before {
6245
+ transform: rotate(90deg);
6246
+ }
6247
+ .fr-turn__content {
6248
+ padding: 0 12px 12px;
6249
+ }
6250
+ .fr-turn__tools {
6251
+ margin: 0;
6252
+ padding-left: 20px;
6253
+ list-style: none;
6254
+ }
6255
+ .fr-turn__tool {
6256
+ margin: 4px 0;
6257
+ font-size: var(--fr-font-sm);
6258
+ color: var(--fr-text-muted);
6259
+ }
6260
+ .fr-turn__tool--error { color: var(--fr-error); }
6261
+
6262
+ .fr-turn__query {
6263
+ color: var(--fr-primary);
6264
+ font-style: italic;
6265
+ }
6266
+
6267
+ .fr-turn__patches {
6268
+ margin: 4px 0 8px 20px;
6269
+ padding: 8px 12px;
6270
+ background: var(--fr-bg-subtle);
6271
+ border-radius: 4px;
6272
+ font-size: var(--fr-font-sm);
6273
+ }
6274
+ .fr-turn__patch {
6275
+ margin: 4px 0;
6276
+ padding: 4px 0;
6277
+ border-bottom: 1px solid var(--fr-border);
6278
+ }
6279
+ .fr-turn__patch:last-child {
6280
+ border-bottom: none;
6281
+ margin-bottom: 0;
6282
+ padding-bottom: 0;
6283
+ }
6284
+ .fr-turn__patch-field {
6285
+ font-weight: 600;
6286
+ color: var(--fr-text);
6287
+ }
6288
+ .fr-turn__patch-op {
6289
+ font-size: 11px;
6290
+ padding: 1px 4px;
6291
+ border-radius: 2px;
6292
+ background: var(--fr-bg-muted);
6293
+ color: var(--fr-text-muted);
6294
+ margin-left: 6px;
6295
+ }
6296
+ .fr-turn__patch-value {
6297
+ display: block;
6298
+ margin-top: 2px;
6299
+ color: var(--fr-text-muted);
6300
+ font-family: ui-monospace, 'SF Mono', Menlo, monospace;
6301
+ word-break: break-word;
6302
+ white-space: pre-wrap;
6303
+ max-height: 200px;
6304
+ overflow: auto;
6305
+ }
6306
+ .fr-turn__patch-value--skip {
6307
+ color: var(--fr-warning);
6308
+ font-style: italic;
6309
+ }
6310
+ .fr-turn__patch-value--clear {
6311
+ color: var(--fr-info);
6312
+ font-style: italic;
6313
+ }
6314
+
6315
+ .fr-raw {
6316
+ position: relative;
6317
+ }
6318
+ .fr-copy-btn {
6319
+ position: absolute;
6320
+ top: 8px;
6321
+ right: 8px;
6322
+ padding: 4px 8px;
6323
+ font-size: var(--fr-font-sm);
6324
+ background: var(--fr-bg-subtle);
6325
+ border: 1px solid var(--fr-border);
6326
+ border-radius: 4px;
6327
+ cursor: pointer;
6328
+ color: var(--fr-text-muted);
6329
+ transition: all 0.15s;
6330
+ }
6331
+ .fr-copy-btn:hover {
6332
+ background: var(--fr-border);
6333
+ color: var(--fr-text);
6334
+ }
6335
+ .fr-copy-btn:active {
6336
+ transform: scale(0.95);
6337
+ }
6338
+
6339
+ /* Scoped pre styles to override parent .tab-content pre */
6340
+ .fr-dashboard pre {
6341
+ background: var(--fr-bg-muted);
6342
+ color: var(--fr-text);
6343
+ padding: 1rem;
6344
+ border-radius: 6px;
6345
+ border: 1px solid var(--fr-border);
6346
+ overflow-x: auto;
6347
+ font-family: ui-monospace, 'SF Mono', Menlo, monospace;
6348
+ font-size: 0.85rem;
6349
+ line-height: 1.5;
6350
+ margin: 0;
6351
+ }
6352
+
6353
+ /* Override syntax highlighting colors for dark mode compatibility */
6354
+ .fr-dashboard .syn-key { color: var(--fr-primary); }
6355
+ .fr-dashboard .syn-string { color: var(--fr-success); }
6356
+ .fr-dashboard .syn-number { color: var(--fr-primary); }
6357
+ .fr-dashboard .syn-bool { color: var(--fr-warning); }
6358
+ .fr-dashboard .syn-null { color: var(--fr-error); }
6359
+
6360
+ @media (max-width: 600px) {
6361
+ .fr-dashboard { padding: 12px; }
6362
+ .fr-cards { grid-template-columns: repeat(2, 1fr); gap: 12px; }
6363
+ .fr-card { padding: 12px; }
6364
+ .fr-card__value { font-size: 18px; }
6365
+ .fr-table { font-size: var(--fr-font-sm); }
6366
+ .fr-table th, .fr-table td { padding: 6px 8px; }
6367
+ }
6368
+ </style>
6369
+ `;
6370
+ /**
6371
+ * Render fill record content (dashboard-style visualization).
6372
+ * Uses CSS custom properties for theming with automatic dark mode support.
6373
+ * Mobile responsive with grid-based layout.
6374
+ *
6375
+ * @public Exported for testing and reuse.
6376
+ */
6377
+ function renderFillRecordContent(record) {
6378
+ const { status, statusDetail, startedAt, durationMs, llm, formProgress, toolSummary, timeline } = record;
6379
+ const startDate = new Date(startedAt);
6380
+ const formattedDate = startDate.toLocaleDateString("en-US", {
6381
+ month: "short",
6382
+ day: "numeric",
6383
+ year: "numeric"
6384
+ });
6385
+ const formattedTime = startDate.toLocaleTimeString("en-US", {
6386
+ hour: "numeric",
6387
+ minute: "2-digit",
6388
+ hour12: true
6389
+ });
6390
+ const headerInfo = `
6391
+ <div class="fr-header">
6392
+ <div class="fr-header__model">${escapeHtml(llm.model)}</div>
6393
+ <div class="fr-header__time">${formattedDate} at ${formattedTime}</div>
6394
+ </div>
6395
+ `;
6396
+ let statusBanner = "";
6397
+ if (status !== "completed") {
6398
+ const bannerClass = status === "failed" ? "fr-banner--error" : "fr-banner--warning";
6399
+ const icon = status === "failed" ? "✕" : "⚠";
6400
+ const title = status === "failed" ? "FAILED" : status === "cancelled" ? "CANCELLED" : "PARTIAL";
6401
+ const msg = statusDetail ?? (status === "partial" ? "Did not complete all fields" : "");
6402
+ statusBanner = `<div class="fr-banner ${bannerClass}"><strong>${icon} ${title}${msg ? ":" : ""}</strong>${msg ? ` ${escapeHtml(msg)}` : ""}</div>`;
6403
+ }
6404
+ const totalTokens = llm.inputTokens + llm.outputTokens;
6405
+ const summaryCards = `
6406
+ <div class="fr-cards">
6407
+ <div class="fr-card">
6408
+ <div class="fr-card__label">Status</div>
6409
+ <div><span class="${`fr-badge fr-badge--${status}`}">${{
6410
+ completed: "✓",
6411
+ partial: "⚠",
6412
+ cancelled: "⊘",
6413
+ failed: "✕"
6414
+ }[status] ?? "?"} ${status.charAt(0).toUpperCase() + status.slice(1)}</span></div>
6415
+ </div>
6416
+ <div class="fr-card">
6417
+ <div class="fr-card__label">Duration</div>
6418
+ <div class="fr-card__value">${formatDuration(durationMs)}</div>
6419
+ </div>
6420
+ <div class="fr-card">
6421
+ <div class="fr-card__label">Turns</div>
6422
+ <div class="fr-card__value">${timeline.length}</div>
6423
+ </div>
6424
+ <div class="fr-card">
6425
+ <div class="fr-card__label">Tokens</div>
6426
+ <div class="fr-card__value">${formatTokens(totalTokens)}</div>
6427
+ <div class="fr-card__sub">${formatTokens(llm.inputTokens)} in / ${formatTokens(llm.outputTokens)} out</div>
6428
+ </div>
6429
+ </div>
6430
+ `;
6431
+ const fieldsMap = /* @__PURE__ */ new Map();
6432
+ for (const turn of timeline) for (const tc of turn.toolCalls) if (tc.tool === "fill_form" && tc.input.patches) {
6433
+ const patches = tc.input.patches;
6434
+ for (const patch of patches) if (patch.fieldId && patch.op) fieldsMap.set(patch.fieldId, {
6435
+ fieldId: patch.fieldId,
6436
+ op: patch.op,
6437
+ turnNumber: turn.turnNumber
6438
+ });
6439
+ }
6440
+ const fieldsFilled = Array.from(fieldsMap.values());
6441
+ const totalFields = formProgress.totalFields;
6442
+ const filledFields = formProgress.filledFields;
6443
+ const skippedFields = formProgress.skippedFields;
6444
+ const abortedFields = formProgress.abortedFields ?? 0;
6445
+ const progressPercent = totalFields > 0 ? Math.round(filledFields / totalFields * 100) : 0;
6446
+ const segmentWidth = totalFields > 0 ? 100 / totalFields : 0;
6447
+ const aiFilledFields = fieldsFilled.filter((f) => f.op !== "skip_field" && f.op !== "abort_field");
6448
+ const aiFilledSegmentsHtml = aiFilledFields.map((f) => {
6449
+ const opLabel = f.op.replace(/_/g, " ");
6450
+ return `<div class="fr-progress-segment fr-progress-segment--filled" style="width: ${segmentWidth}%" data-tooltip="${escapeHtml(`${f.fieldId}\n${opLabel}\nTurn ${f.turnNumber}`)}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>`;
6451
+ }).join("");
6452
+ const prefilledCount = Math.max(0, filledFields - aiFilledFields.length);
6453
+ const prefilledSegmentsHtml = prefilledCount > 0 ? `<div class="fr-progress-segment fr-progress-segment--prefilled" style="width: ${segmentWidth * prefilledCount}%" data-tooltip="Pre-filled (${prefilledCount} field${prefilledCount !== 1 ? "s" : ""})" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>` : "";
6454
+ const skippedSegmentsHtml = fieldsFilled.filter((f) => f.op === "skip_field" || f.op === "abort_field").map((f) => {
6455
+ const opLabel = f.op === "skip_field" ? "skipped" : "aborted";
6456
+ return `<div class="fr-progress-segment fr-progress-segment--skipped" style="width: ${segmentWidth}%" data-tooltip="${escapeHtml(`${f.fieldId}\n${opLabel}\nTurn ${f.turnNumber}`)}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>`;
6457
+ }).join("");
6458
+ const unfilledCount = totalFields - filledFields - skippedFields - abortedFields;
6459
+ const unfilledSegmentsHtml = unfilledCount > 0 ? `<div class="fr-progress-segment fr-progress-segment--empty" style="width: ${segmentWidth * unfilledCount}%"></div>` : "";
6460
+ const progressDetails = [];
6461
+ if (prefilledCount > 0) progressDetails.push(`${prefilledCount} pre-filled`);
6462
+ if (skippedFields > 0) progressDetails.push(`${skippedFields} skipped`);
6463
+ const progressBar = `
6464
+ <div class="fr-section">
6465
+ <div class="fr-section__title">Progress</div>
6466
+ <div class="fr-progress">
6467
+ <div class="fr-progress__segments">
6468
+ ${prefilledSegmentsHtml}${aiFilledSegmentsHtml}${skippedSegmentsHtml}${unfilledSegmentsHtml}
6469
+ </div>
6470
+ </div>
6471
+ <div class="fr-progress__text">
6472
+ ${filledFields}/${totalFields} fields filled (${progressPercent}%)${progressDetails.length > 0 ? ` • ${progressDetails.join(" • ")}` : ""}
6473
+ </div>
6474
+ </div>
6475
+ `;
6476
+ const totalMs = durationMs;
6477
+ const llmCallCount = llm.totalCalls;
6478
+ const toolCallCount = toolSummary.totalCalls;
6479
+ const timelineEvents = [];
6480
+ let currentTime = 0;
6481
+ for (const turn of timeline) {
6482
+ const toolTimeInTurn = turn.toolCalls.reduce((sum, tc) => sum + tc.durationMs, 0);
6483
+ const llmTimeInTurn = Math.max(0, turn.durationMs - toolTimeInTurn);
6484
+ if (llmTimeInTurn > 0) timelineEvents.push({
6485
+ type: "llm",
6486
+ startMs: currentTime,
6487
+ durationMs: llmTimeInTurn,
6488
+ turnNumber: turn.turnNumber,
6489
+ label: `Turn ${turn.turnNumber}`,
6490
+ tokens: {
6491
+ input: turn.tokens.input,
6492
+ output: turn.tokens.output,
6493
+ total: turn.tokens.input + turn.tokens.output
6494
+ }
6495
+ });
6496
+ currentTime += llmTimeInTurn;
6497
+ for (const tc of turn.toolCalls) {
6498
+ timelineEvents.push({
6499
+ type: "tool",
6500
+ startMs: currentTime,
6501
+ durationMs: tc.durationMs,
6502
+ turnNumber: turn.turnNumber,
6503
+ label: tc.tool
6504
+ });
6505
+ currentTime += tc.durationMs;
6506
+ }
6507
+ }
6508
+ const ganttRowsHtml = timelineEvents.map((e) => {
6509
+ const leftPct = totalMs > 0 ? e.startMs / totalMs * 100 : 0;
6510
+ const widthPct = totalMs > 0 ? e.durationMs / totalMs * 100 : 0;
6511
+ const barClass = e.type === "llm" ? "fr-gantt__bar--llm" : "fr-gantt__bar--tool";
6512
+ const tooltip = e.type === "llm" ? `${e.label}&#10;${formatDuration(e.durationMs)}&#10;${formatTokens(e.tokens?.total ?? 0)} tokens (${formatTokens(e.tokens?.input ?? 0)} in / ${formatTokens(e.tokens?.output ?? 0)} out)` : `${e.label}&#10;${formatDuration(e.durationMs)}&#10;Turn ${e.turnNumber}`;
6513
+ return `
6514
+ <div class="fr-gantt__row">
6515
+ <div class="fr-gantt__label">${escapeHtml(e.label)}</div>
6516
+ <div class="fr-gantt__track">
6517
+ <div class="fr-gantt__bar ${barClass}" style="left: ${leftPct}%; width: ${widthPct}%" data-tooltip="${tooltip}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>
6518
+ </div>
6519
+ </div>`;
6520
+ }).join("");
6521
+ const llmTotalMs = timelineEvents.filter((e) => e.type === "llm").reduce((sum, e) => sum + e.durationMs, 0);
6522
+ const toolTotalMs = timelineEvents.filter((e) => e.type === "tool").reduce((sum, e) => sum + e.durationMs, 0);
6523
+ const timingSection = `
6524
+ <details class="fr-details fr-section" open>
6525
+ <summary>Timeline (${formatDuration(totalMs)} total)</summary>
6526
+ <div class="fr-details__content">
6527
+ <div class="fr-gantt">
6528
+ ${ganttRowsHtml}
6529
+ <div class="fr-gantt__legend">
6530
+ <div class="fr-gantt__legend-item">
6531
+ <div class="fr-gantt__legend-dot fr-gantt__legend-dot--llm"></div>
6532
+ <span>LLM (${llmCallCount} call${llmCallCount !== 1 ? "s" : ""}, ${formatDuration(llmTotalMs)})</span>
6533
+ </div>
6534
+ <div class="fr-gantt__legend-item">
6535
+ <div class="fr-gantt__legend-dot fr-gantt__legend-dot--tool"></div>
6536
+ <span>Tools (${toolCallCount} call${toolCallCount !== 1 ? "s" : ""}, ${formatDuration(toolTotalMs)})</span>
6537
+ </div>
6538
+ </div>
6539
+ </div>
6540
+ </div>
6541
+ </details>
6542
+ `;
6543
+ let toolSection = "";
6544
+ if (toolSummary.byTool.length > 0) toolSection = `
6545
+ <details class="fr-details fr-section" open>
6546
+ <summary>Tool Summary</summary>
6547
+ <div style="overflow-x: auto; margin-top: 8px;">
6548
+ <table class="fr-table">
6549
+ <thead><tr><th>Tool</th><th>Calls</th><th>Success</th><th>Avg</th><th>p95</th></tr></thead>
6550
+ <tbody>${toolSummary.byTool.map((t) => `
6551
+ <tr>
6552
+ <td>${escapeHtml(t.toolName)}</td>
6553
+ <td>${t.callCount}</td>
6554
+ <td>${t.successCount === t.callCount ? "100%" : `${Math.round(t.successCount / t.callCount * 100)}%`}</td>
6555
+ <td>${formatDuration(t.timing.avgMs)}</td>
6556
+ <td>${formatDuration(t.timing.p95Ms)}</td>
6557
+ </tr>
6558
+ `).join("")}</tbody>
6559
+ </table>
6560
+ </div>
6561
+ </details>
6562
+ `;
6563
+ let timelineSection = "";
6564
+ if (timeline.length > 0) {
6565
+ const timelineItems = timeline.map((turn) => {
6566
+ const turnTokens = turn.tokens.input + turn.tokens.output;
6567
+ const toolCallsList = turn.toolCalls.map((tc) => renderToolCall(tc)).join("");
6568
+ const patchInfo = turn.patchesApplied > 0 ? ` • ${turn.patchesApplied} patches` : "";
6569
+ const rejectedInfo = turn.patchesRejected > 0 ? ` <span style="color: var(--fr-error)">(${turn.patchesRejected} rejected)</span>` : "";
6570
+ return `
6571
+ <details class="fr-turn">
6572
+ <summary><strong>Turn ${turn.turnNumber}</strong> • Order ${turn.order} • ${formatDuration(turn.durationMs)} • ${formatTokens(turnTokens)} tokens${patchInfo}${rejectedInfo}</summary>
6573
+ <div class="fr-turn__content">
6574
+ ${turn.toolCalls.length > 0 ? `<ul class="fr-turn__tools">${toolCallsList}</ul>` : "<span class=\"fr-turn__tool\">No tool calls</span>"}
6575
+ </div>
6576
+ </details>
6577
+ `;
6578
+ }).join("");
6579
+ timelineSection = `
6580
+ <details class="fr-details fr-section">
6581
+ <summary>Turn Details (${timeline.length} turns)</summary>
6582
+ <div style="margin-top: 8px;">${timelineItems}</div>
6583
+ </details>
6584
+ `;
6585
+ }
6586
+ const rawSection = `
6587
+ <details class="fr-details fr-section">
6588
+ <summary>Raw YAML</summary>
6589
+ <div class="fr-raw" style="margin-top: 8px;">
6590
+ <button class="fr-copy-btn" onclick="frCopyYaml(this)">Copy</button>
6591
+ ${renderYamlContent(YAML.stringify(record, { lineWidth: 0 }))}
6592
+ </div>
6593
+ </details>
6594
+ `;
6595
+ return `
6596
+ ${FILL_RECORD_STYLES}
6597
+ <div id="fr-tooltip" class="fr-tooltip"></div>
6598
+ <div class="fr-dashboard">
6599
+ ${headerInfo}
6600
+ ${statusBanner}
6601
+ ${summaryCards}
6602
+ ${progressBar}
6603
+ ${timingSection}
6604
+ ${toolSection}
6605
+ ${timelineSection}
6606
+ ${rawSection}
6607
+ </div>
6608
+ `;
6609
+ }
5659
6610
 
5660
6611
  //#endregion
5661
6612
  //#region src/cli/commands/render.ts
@@ -5779,7 +6730,7 @@ function registerResearchCommand(program) {
5779
6730
  const modelId = options.model;
5780
6731
  const { provider, model: modelName } = parseModelIdForDisplay(modelId);
5781
6732
  if (!hasWebSearchSupport(provider)) {
5782
- const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
6733
+ const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p]) => p);
5783
6734
  logError(`Model "${modelId}" does not support web search.`);
5784
6735
  console.log("");
5785
6736
  console.log(pc.yellow("Research forms require web search capabilities."));
@@ -5827,6 +6778,7 @@ function registerResearchCommand(program) {
5827
6778
  model: modelId,
5828
6779
  enableWebSearch: true,
5829
6780
  captureWireFormat: false,
6781
+ recordFill: false,
5830
6782
  maxTurnsTotal: maxTurns,
5831
6783
  maxPatchesPerTurn,
5832
6784
  maxIssuesPerTurn,
@@ -5849,10 +6801,10 @@ function registerResearchCommand(program) {
5849
6801
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
5850
6802
  console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
5851
6803
  if (options.transcript && result.transcript) {
5852
- const { serializeSession: serializeSession$1 } = await import("./session-CW9AQw6i.mjs");
6804
+ const { serializeSession } = await import("./session-ZHBi3LVQ.mjs");
5853
6805
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
5854
- const { writeFile: writeFile$1 } = await import("./shared-fUKfJ1UA.mjs");
5855
- await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
6806
+ const { writeFile } = await import("./shared-BTR35aMz.mjs");
6807
+ await writeFile(transcriptPath, serializeSession(result.transcript));
5856
6808
  logInfo(ctx, `Transcript: ${transcriptPath}`);
5857
6809
  }
5858
6810
  logTiming(ctx, "Research fill", Date.now() - startTime);
@@ -5927,7 +6879,6 @@ function computeFieldStats(form, fields) {
5927
6879
  case "aborted":
5928
6880
  aborted++;
5929
6881
  break;
5930
- case "unanswered":
5931
6882
  default:
5932
6883
  unanswered++;
5933
6884
  break;
@@ -6094,7 +7045,6 @@ function formatPriority(priority, useColors) {
6094
7045
  case 2: return pc.yellow(label);
6095
7046
  case 3: return pc.cyan(label);
6096
7047
  case 4: return pc.blue(label);
6097
- case 5:
6098
7048
  default: return pc.dim(label);
6099
7049
  }
6100
7050
  }
@@ -6258,4 +7208,4 @@ async function runCli() {
6258
7208
 
6259
7209
  //#endregion
6260
7210
  export { runCli as t };
6261
- //# sourceMappingURL=cli-D9w0Bp4J.mjs.map
7211
+ //# sourceMappingURL=cli-FFMoEhFS.mjs.map