markform 0.1.20 → 0.1.22

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 (58) hide show
  1. package/README.md +44 -12
  2. package/dist/ai-sdk.d.mts +1 -1
  3. package/dist/ai-sdk.mjs +2 -2
  4. package/dist/{apply-DIvm1b1s.mjs → apply-C7mO7VkZ.mjs} +158 -95
  5. package/dist/apply-C7mO7VkZ.mjs.map +1 -0
  6. package/dist/bin.mjs +1 -1
  7. package/dist/{cli-FFMoEhFS.mjs → cli-C8F9yDsv.mjs} +102 -1225
  8. package/dist/cli-C8F9yDsv.mjs.map +1 -0
  9. package/dist/cli.mjs +1 -1
  10. package/dist/{coreTypes-CkxML8g2.d.mts → coreTypes-BlsJkU1w.d.mts} +28 -2
  11. package/dist/{coreTypes-CPKXf2dc.mjs → coreTypes-CTLr-NGd.mjs} +24 -3
  12. package/dist/coreTypes-CTLr-NGd.mjs.map +1 -0
  13. package/dist/fillRecord-DTl5lnK0.d.mts +345 -0
  14. package/dist/fillRecordRenderer-CruJrLkj.mjs +1256 -0
  15. package/dist/fillRecordRenderer-CruJrLkj.mjs.map +1 -0
  16. package/dist/index.d.mts +22 -342
  17. package/dist/index.mjs +5 -5
  18. package/dist/render.d.mts +74 -0
  19. package/dist/render.mjs +4 -0
  20. package/dist/{session-CK0x28RO.mjs → session-BCcltrLA.mjs} +2 -2
  21. package/dist/{session-CK0x28RO.mjs.map → session-BCcltrLA.mjs.map} +1 -1
  22. package/dist/{session-ZHBi3LVQ.mjs → session-VeSkVrck.mjs} +1 -1
  23. package/dist/{shared-DwdyWmvE.mjs → shared-CsdT2T7k.mjs} +1 -1
  24. package/dist/{shared-DwdyWmvE.mjs.map → shared-CsdT2T7k.mjs.map} +1 -1
  25. package/dist/{shared-BTR35aMz.mjs → shared-fb0nkzQi.mjs} +1 -1
  26. package/dist/{src-wR7GoftB.mjs → src-CbRnGzMK.mjs} +205 -138
  27. package/dist/src-CbRnGzMK.mjs.map +1 -0
  28. package/dist/urlFormat-lls7CsEP.mjs +71 -0
  29. package/dist/urlFormat-lls7CsEP.mjs.map +1 -0
  30. package/docs/markform-apis.md +53 -0
  31. package/examples/movie-research/movie-deep-research-mock-filled.form.md +320 -343
  32. package/examples/movie-research/movie-deep-research.form.md +273 -308
  33. package/examples/movie-research/movie-research-demo.form.md +27 -41
  34. package/examples/parallel/parallel-research.form.md +33 -29
  35. package/examples/parallel/parallel-research.mock.filled.form.md +88 -0
  36. package/examples/rejection-test/rejection-test-mock-filled.form.md +21 -16
  37. package/examples/rejection-test/rejection-test-mock-filled.schema.json +1 -1
  38. package/examples/rejection-test/rejection-test.form.md +17 -15
  39. package/examples/rejection-test/rejection-test.session.yaml +88 -60
  40. package/examples/simple/simple-mock-filled.form.md +113 -126
  41. package/examples/simple/simple-mock-filled.schema.json +2 -3
  42. package/examples/simple/simple-skipped-filled.form.md +112 -129
  43. package/examples/simple/simple-skipped-filled.report.md +8 -8
  44. package/examples/simple/simple-skipped-filled.schema.json +2 -3
  45. package/examples/simple/simple-tags-syntax.form.md +32 -0
  46. package/examples/simple/simple-with-skips.session.yaml +663 -627
  47. package/examples/simple/simple.form.md +97 -113
  48. package/examples/simple/simple.schema.json +2 -3
  49. package/examples/simple/simple.session.yaml +663 -627
  50. package/examples/startup-deep-research/startup-deep-research.form.md +191 -235
  51. package/examples/startup-research/startup-research-mock-filled.form.md +128 -147
  52. package/examples/startup-research/startup-research.form.md +90 -129
  53. package/examples/twitter-thread/twitter-thread.form.md +373 -0
  54. package/package.json +5 -1
  55. package/dist/apply-DIvm1b1s.mjs.map +0 -1
  56. package/dist/cli-FFMoEhFS.mjs.map +0 -1
  57. package/dist/coreTypes-CPKXf2dc.mjs.map +0 -1
  58. package/dist/src-wR7GoftB.mjs.map +0 -1
@@ -1,9 +1,10 @@
1
1
 
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";
2
+ import { R as PatchSchema } from "./coreTypes-CTLr-NGd.mjs";
3
+ import { $ as WEB_SEARCH_CONFIG, A as DEFAULT_FORMS_DIR, F as DEFAULT_MAX_TURNS, G as deriveExportPath, H as MAX_FORMS_IN_MENU, I as DEFAULT_PORT, J as deriveSchemaPath, K as deriveFillRecordPath, N as DEFAULT_MAX_PATCHES_PER_TURN, Q as SUGGESTED_LLMS, R as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, U as REPORT_EXTENSION, W as USER_ROLE, X as parseRolesFlag, Y as detectFileType, c as computeProgressSummary, d as serializeForm, et as formatSuggestedLlms, f as serializeRawMarkdown, g as validateSyntaxConsistency, i as inspect, j as DEFAULT_MAX_ISSUES_PER_TURN, k as AGENT_ROLE, l as computeStructureSummary, n as getAllFields, nt as hasWebSearchSupport, p as serializeReport, q as deriveReportPath, rt as parseModelIdForDisplay, t as applyPatches, z as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-C7mO7VkZ.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-CbRnGzMK.mjs";
5
+ import { n as serializeSession } from "./session-BCcltrLA.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-CsdT2T7k.mjs";
7
+ import { a as renderJsonContent, c as renderViewContent, i as highlightYamlValue, l as renderYamlContent, o as renderMarkdownContent, r as renderFillRecordContent, s as renderSourceContent, u as escapeHtml } from "./fillRecordRenderer-CruJrLkj.mjs";
7
8
  import Markdoc from "@markdoc/markdoc";
8
9
  import YAML from "yaml";
9
10
  import { Command } from "commander";
@@ -1947,7 +1948,7 @@ function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
1947
1948
  /**
1948
1949
  * Format a patch value for display with truncation.
1949
1950
  */
1950
- function formatPatchValue$1(patch) {
1951
+ function formatPatchValue(patch) {
1951
1952
  switch (patch.op) {
1952
1953
  case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
1953
1954
  case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
@@ -2052,7 +2053,7 @@ function createFillLoggingCallbacks(ctx, options = {}) {
2052
2053
  logInfo(ctx, ` -> ${pc.yellow(String(patches.length))} patch(es):`);
2053
2054
  for (const patch of patches) {
2054
2055
  const typeName = formatPatchType(patch);
2055
- const value = formatPatchValue$1(patch);
2056
+ const value = formatPatchValue(patch);
2056
2057
  const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2057
2058
  if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2058
2059
  else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
@@ -2638,7 +2639,10 @@ function registerExamplesCommand(program) {
2638
2639
  * Register the export command.
2639
2640
  */
2640
2641
  function registerExportCommand(program) {
2641
- program.command("export <file>").description("Export form as markform (default), markdown (readable), or json/yaml for structured data").option("--compact", "Output compact JSON (no formatting, only for JSON format)").option("--normalize", "Regenerate form without preserving external content").action(async (file, options, cmd) => {
2642
+ program.command("export <file>").description("Export form as markform (default), markdown (readable), or json/yaml for structured data").option("--compact", "Output compact JSON (no formatting, only for JSON format)").option("--normalize", "Regenerate form without preserving external content").option("--syntax <style>", "Output syntax style: comments (HTML comments) or tags (Markdoc). Default: preserve original.", (value) => {
2643
+ if (value !== "comments" && value !== "tags") throw new Error(`Invalid syntax value: ${value}. Must be 'comments' or 'tags'.`);
2644
+ return value;
2645
+ }).action(async (file, options, cmd) => {
2642
2646
  const ctx = getCommandContext(cmd);
2643
2647
  let format = "markform";
2644
2648
  if (ctx.format === "json") format = "json";
@@ -2651,7 +2655,10 @@ function registerExportCommand(program) {
2651
2655
  logVerbose(ctx, "Parsing form...");
2652
2656
  const form = parseForm(content);
2653
2657
  if (format === "markform") {
2654
- console.log(serializeForm(form, { preserveContent: !options.normalize }));
2658
+ console.log(serializeForm(form, {
2659
+ preserveContent: !options.normalize,
2660
+ syntaxStyle: options.syntax
2661
+ }));
2655
2662
  return;
2656
2663
  }
2657
2664
  if (format === "markdown") {
@@ -2684,7 +2691,10 @@ function registerExportCommand(program) {
2684
2691
  schema,
2685
2692
  values,
2686
2693
  notes: form.notes,
2687
- markdown: serializeForm(form, { preserveContent: !options.normalize })
2694
+ markdown: serializeForm(form, {
2695
+ preserveContent: !options.normalize,
2696
+ syntaxStyle: options.syntax
2697
+ })
2688
2698
  };
2689
2699
  if (format === "json") if (options.compact) console.log(JSON.stringify(output));
2690
2700
  else console.log(JSON.stringify(output, null, 2));
@@ -2769,7 +2779,7 @@ function formatConsoleSession(transcript, useColors) {
2769
2779
  * Register the fill command.
2770
2780
  */
2771
2781
  function registerFillCommand(program) {
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) => {
2782
+ 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)").option("--parallel", "Enable parallel execution for forms with parallel batches").action(async (file, options, cmd) => {
2773
2783
  const ctx = getCommandContext(cmd);
2774
2784
  const filePath = resolve(file);
2775
2785
  let harness;
@@ -2861,6 +2871,74 @@ function registerFillCommand(program) {
2861
2871
  process.exit(1);
2862
2872
  }
2863
2873
  if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
2874
+ if (options.parallel && !options.mock) {
2875
+ logInfo(ctx, pc.cyan(`Filling form (parallel): ${filePath}`));
2876
+ logInfo(ctx, `Agent: live (${options.model})`);
2877
+ let systemPrompt;
2878
+ if (options.instructions) systemPrompt = options.instructions;
2879
+ else if (options.prompt) systemPrompt = await readFile$1(resolve(options.prompt));
2880
+ let outputPath;
2881
+ if (options.output) outputPath = resolve(options.output);
2882
+ else {
2883
+ const formsDir = getFormsDir(ctx.formsDir);
2884
+ await ensureFormsDir(formsDir);
2885
+ outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
2886
+ }
2887
+ const result = await fillForm({
2888
+ form,
2889
+ model: options.model,
2890
+ enableParallel: true,
2891
+ enableWebSearch: true,
2892
+ captureWireFormat: false,
2893
+ recordFill: true,
2894
+ targetRoles,
2895
+ fillMode,
2896
+ maxTurnsTotal: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
2897
+ maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
2898
+ maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
2899
+ systemPromptAddition: systemPrompt,
2900
+ callbacks: {
2901
+ onTurnStart: ({ turnNumber, executionId }) => {
2902
+ logInfo(ctx, `${pc.bold(`Turn ${turnNumber}:`)} started (${pc.dim(executionId)})`);
2903
+ },
2904
+ onTurnComplete: (progress) => {
2905
+ const patchText = progress.patchesApplied === 1 ? "patch" : "patches";
2906
+ logInfo(ctx, ` → ${pc.yellow(String(progress.patchesApplied))} ${patchText} applied`);
2907
+ if (progress.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
2908
+ },
2909
+ onBatchStart: ({ batchId, itemCount }) => {
2910
+ logInfo(ctx, pc.cyan(`Starting batch "${batchId}" with ${itemCount} items`));
2911
+ },
2912
+ onBatchComplete: ({ batchId, patchesApplied }) => {
2913
+ logInfo(ctx, pc.cyan(`Batch "${batchId}" completed: ${patchesApplied} patches`));
2914
+ }
2915
+ }
2916
+ });
2917
+ const durationMs = Date.now() - startTime;
2918
+ if (result.status.ok) logSuccess(ctx, `Form completed in ${result.turns} turn(s)`);
2919
+ else logWarn(ctx, `Fill incomplete: ${result.status.reason}`);
2920
+ logTiming(ctx, "Fill time", durationMs);
2921
+ if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
2922
+ else {
2923
+ await writeFile(outputPath, result.markdown);
2924
+ logSuccess(ctx, `Form written to: ${outputPath}`);
2925
+ }
2926
+ if (result.record && !ctx.quiet) {
2927
+ console.log("");
2928
+ const summary = formatFillRecordSummary(result.record, { verbose: ctx.verbose });
2929
+ console.error(summary);
2930
+ }
2931
+ if ((options.recordFill || options.recordFillStable) && result.record) {
2932
+ const sidecarPath = deriveFillRecordPath(outputPath);
2933
+ const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(result.record) : result.record;
2934
+ if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write fill record to: ${sidecarPath}`);
2935
+ else {
2936
+ writeFileSync(sidecarPath, JSON.stringify(recordToWrite, null, 2));
2937
+ logSuccess(ctx, `Fill record written to: ${sidecarPath}`);
2938
+ }
2939
+ }
2940
+ process.exit(result.status.ok ? 0 : 1);
2941
+ }
2864
2942
  const cliOptions = {
2865
2943
  maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
2866
2944
  maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
@@ -2935,7 +3013,7 @@ function registerFillCommand(program) {
2935
3013
  turnNumber: turn.turnNumber,
2936
3014
  issuesCount: turn.issuesCount,
2937
3015
  order: turn.order ?? 0,
2938
- executionId: turn.executionId ?? "cli-serial"
3016
+ executionId: turn.executionId ?? "eid:serial:o0"
2939
3017
  });
2940
3018
  },
2941
3019
  onTurnComplete: (progress) => {
@@ -2986,7 +3064,7 @@ function registerFillCommand(program) {
2986
3064
  turnNumber: stepResult.turnNumber,
2987
3065
  issuesCount: stepResult.issues.length,
2988
3066
  order: 0,
2989
- executionId: "cli-serial"
3067
+ executionId: "eid:serial:o0"
2990
3068
  });
2991
3069
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2992
3070
  let spinner = null;
@@ -3014,7 +3092,7 @@ function registerFillCommand(program) {
3014
3092
  logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
3015
3093
  for (const patch of patches) {
3016
3094
  const typeName = formatPatchType(patch);
3017
- const value = formatPatchValue$1(patch);
3095
+ const value = formatPatchValue(patch);
3018
3096
  const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
3019
3097
  if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
3020
3098
  else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
@@ -3076,7 +3154,7 @@ function registerFillCommand(program) {
3076
3154
  turnNumber: stepResult.turnNumber,
3077
3155
  issuesCount: stepResult.issues.length,
3078
3156
  order: 0,
3079
- executionId: "cli-serial"
3157
+ executionId: "eid:serial:o0"
3080
3158
  });
3081
3159
  }
3082
3160
  }
@@ -4956,7 +5034,9 @@ function renderGroup(group, responses) {
4956
5034
  const groupTitle = group.title ?? group.id;
4957
5035
  const fieldsHtml = group.children.map((field) => {
4958
5036
  const response = responses[field.id];
4959
- return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
5037
+ const value = response?.state === "answered" ? response.value : void 0;
5038
+ const isSkipped = response?.state === "skipped";
5039
+ return renderFieldHtml(field, value, isSkipped, isSkipped ? response?.reason : void 0);
4960
5040
  }).join("\n");
4961
5041
  return `
4962
5042
  <div class="group">
@@ -4968,11 +5048,12 @@ function renderGroup(group, responses) {
4968
5048
  * Render a field as HTML.
4969
5049
  * @public Exported for testing.
4970
5050
  */
4971
- function renderFieldHtml(field, value, isSkipped) {
5051
+ function renderFieldHtml(field, value, isSkipped, skipReason) {
4972
5052
  const skipped = isSkipped === true;
4973
5053
  const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
4974
5054
  const typeLabel = `<span class="type-badge">${field.kind}</span>`;
4975
- const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
5055
+ const skippedText = skipped && skipReason ? `Skipped: ${escapeHtml(skipReason)}` : "Skipped";
5056
+ const skippedBadge = skipped ? `<span class="skipped-badge">${skippedText}</span>` : "";
4976
5057
  const fieldClass = skipped ? "field field-skipped" : "field";
4977
5058
  const disabledAttr = skipped ? " disabled" : "";
4978
5059
  let inputHtml;
@@ -5205,13 +5286,6 @@ function renderTableInput(field, value, _disabledAttr) {
5205
5286
  <div class="field-help">(table fields are currently read-only in the web UI)</div>
5206
5287
  </div>`;
5207
5288
  }
5208
- /**
5209
- * Escape HTML special characters.
5210
- * @public Exported for testing.
5211
- */
5212
- function escapeHtml(str) {
5213
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
5214
- }
5215
5289
  /** Common styles for read-only viewers */
5216
5290
  const READ_ONLY_STYLES = `
5217
5291
  * { box-sizing: border-box; }
@@ -5363,17 +5437,6 @@ function renderYamlHtml(content, filename) {
5363
5437
  </html>`;
5364
5438
  }
5365
5439
  /**
5366
- * Highlight a YAML value based on its type.
5367
- */
5368
- function highlightYamlValue(value) {
5369
- const trimmed = value.trim();
5370
- if (trimmed === "true" || trimmed === "false") return `<span class="syn-bool">${escapeHtml(value)}</span>`;
5371
- if (trimmed === "null" || trimmed === "~") return `<span class="syn-null">${escapeHtml(value)}</span>`;
5372
- if (/^-?\d+\.?\d*$/.test(trimmed)) return `<span class="syn-number">${escapeHtml(value)}</span>`;
5373
- if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return `<span class="syn-string">${escapeHtml(value)}</span>`;
5374
- return `<span class="syn-string">${escapeHtml(value)}</span>`;
5375
- }
5376
- /**
5377
5440
  * Render JSON content with syntax highlighting and formatting.
5378
5441
  */
5379
5442
  function renderJsonHtml(content, filename) {
@@ -5421,1192 +5484,6 @@ function renderPlainTextHtml(content, filename) {
5421
5484
  </body>
5422
5485
  </html>`;
5423
5486
  }
5424
- /**
5425
- * Render form view content (read-only display of form fields).
5426
- * Used for View tab content.
5427
- * @public Exported for testing.
5428
- */
5429
- function renderViewContent(form) {
5430
- const { schema, responsesByFieldId } = form;
5431
- let html = "<div class=\"view-content\">";
5432
- for (const group of schema.groups) {
5433
- const groupTitle = group.title ?? group.id;
5434
- html += `<div class="view-group"><h2>${escapeHtml(groupTitle)}</h2>`;
5435
- for (const field of group.children) {
5436
- const response = responsesByFieldId[field.id];
5437
- const value = response?.state === "answered" ? response.value : void 0;
5438
- const isSkipped = response?.state === "skipped";
5439
- html += "<div class=\"view-field\">";
5440
- html += `<div class="view-field-label">${escapeHtml(field.label)}`;
5441
- html += ` <span class="type-badge">${field.kind}</span>`;
5442
- if (field.required) html += " <span class=\"required\">*</span>";
5443
- if (isSkipped) html += " <span class=\"skipped-badge\">Skipped</span>";
5444
- html += "</div>";
5445
- html += renderViewFieldValue(field, value, isSkipped);
5446
- html += "</div>";
5447
- }
5448
- html += "</div>";
5449
- }
5450
- html += "</div>";
5451
- return html;
5452
- }
5453
- /**
5454
- * Format a checkbox state for display.
5455
- */
5456
- function formatCheckboxState(state) {
5457
- switch (state) {
5458
- case "done": return "<span class=\"checkbox checked\">☑</span>";
5459
- case "todo": return "<span class=\"checkbox unchecked\">☐</span>";
5460
- case "active": return "<span class=\"state-badge state-active\">●</span>";
5461
- case "incomplete": return "<span class=\"state-badge state-incomplete\">○</span>";
5462
- case "na": return "<span class=\"state-badge state-na\">—</span>";
5463
- case "yes": return "<span class=\"checkbox checked\">☑</span>";
5464
- case "no": return "<span class=\"checkbox unchecked\">☐</span>";
5465
- case "unfilled": return "<span class=\"state-badge state-unfilled\">?</span>";
5466
- default: return `<span class="state-badge">${escapeHtml(state)}</span>`;
5467
- }
5468
- }
5469
- /**
5470
- * Render a field value for the View tab.
5471
- */
5472
- function renderViewFieldValue(field, value, isSkipped) {
5473
- if (isSkipped) return "<div class=\"view-field-empty\">(skipped)</div>";
5474
- if (value === void 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5475
- switch (field.kind) {
5476
- case "string": {
5477
- const v = value.kind === "string" ? value.value : null;
5478
- if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5479
- return `<div class="view-field-value">${formatBareUrlsAsHtmlLinks(v, escapeHtml)}</div>`;
5480
- }
5481
- case "number": {
5482
- const v = value.kind === "number" ? value.value : null;
5483
- if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
5484
- return `<div class="view-field-value">${v}</div>`;
5485
- }
5486
- case "string_list": {
5487
- const items = value.kind === "string_list" ? value.items : [];
5488
- if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5489
- return `<div class="view-field-value"><ul>${items.map((i) => `<li>${formatBareUrlsAsHtmlLinks(i, escapeHtml)}</li>`).join("")}</ul></div>`;
5490
- }
5491
- case "single_select": {
5492
- const selected = value.kind === "single_select" ? value.selected : null;
5493
- if (selected === null) return "<div class=\"view-field-empty\">(not filled)</div>";
5494
- return `<div class="view-field-value">${escapeHtml(field.options.find((o) => o.id === selected)?.label ?? selected)}</div>`;
5495
- }
5496
- case "multi_select": {
5497
- const selected = value.kind === "multi_select" ? value.selected : [];
5498
- return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
5499
- return `<li class="checkbox-item">${selected.includes(opt.id) ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
5500
- }).join("")}</ul></div>`;
5501
- }
5502
- case "checkboxes": {
5503
- const values = value.kind === "checkboxes" ? value.values : {};
5504
- const mode = field.checkboxMode ?? "multi";
5505
- return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
5506
- const state = values[opt.id] ?? (mode === "explicit" ? "unfilled" : "todo");
5507
- if (mode === "simple") return `<li class="checkbox-item">${state === "done" ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
5508
- return `<li class="checkbox-item">${formatCheckboxState(state)} ${escapeHtml(opt.label)}</li>`;
5509
- }).join("")}</ul></div>`;
5510
- }
5511
- case "url": {
5512
- const v = value.kind === "url" ? value.value : null;
5513
- if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5514
- const domain = friendlyUrlAbbrev(v);
5515
- return `<div class="view-field-value"><a href="${escapeHtml(v)}" target="_blank" class="url-link" data-url="${escapeHtml(v)}">${escapeHtml(domain)}</a></div>`;
5516
- }
5517
- case "url_list": {
5518
- const items = value.kind === "url_list" ? value.items : [];
5519
- if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5520
- return `<div class="view-field-value"><ul>${items.map((u) => {
5521
- const domain = friendlyUrlAbbrev(u);
5522
- return `<li><a href="${escapeHtml(u)}" target="_blank" class="url-link" data-url="${escapeHtml(u)}">${escapeHtml(domain)}</a></li>`;
5523
- }).join("")}</ul></div>`;
5524
- }
5525
- case "date": {
5526
- const v = value.kind === "date" ? value.value : null;
5527
- if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5528
- return `<div class="view-field-value">${escapeHtml(v)}</div>`;
5529
- }
5530
- case "year": {
5531
- const v = value.kind === "year" ? value.value : null;
5532
- if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
5533
- return `<div class="view-field-value">${v}</div>`;
5534
- }
5535
- case "table": {
5536
- const rows = value.kind === "table" ? value.rows : [];
5537
- if (rows.length === 0) return "<div class=\"view-field-empty\">(no data)</div>";
5538
- let tableHtml = "<div class=\"table-container\"><table class=\"data-table\">";
5539
- tableHtml += "<thead><tr>";
5540
- for (const col of field.columns) tableHtml += `<th>${escapeHtml(col.label)}</th>`;
5541
- tableHtml += "</tr></thead><tbody>";
5542
- for (const row of rows) {
5543
- tableHtml += "<tr>";
5544
- for (const col of field.columns) {
5545
- const cell = row[col.id];
5546
- let cellValue = "";
5547
- let cellHtml = "";
5548
- if (cell?.state === "answered" && cell.value !== void 0 && cell.value !== null) {
5549
- cellValue = String(cell.value);
5550
- if (col.type === "url" && cellValue) {
5551
- const domain = friendlyUrlAbbrev(cellValue);
5552
- cellHtml = `<a href="${escapeHtml(cellValue)}" target="_blank" class="url-link" data-url="${escapeHtml(cellValue)}">${escapeHtml(domain)}</a>`;
5553
- } else cellHtml = formatBareUrlsAsHtmlLinks(cellValue, escapeHtml);
5554
- }
5555
- tableHtml += `<td>${cellHtml}</td>`;
5556
- }
5557
- tableHtml += "</tr>";
5558
- }
5559
- tableHtml += "</tbody></table></div>";
5560
- return tableHtml;
5561
- }
5562
- default: {
5563
- const _exhaustive = field;
5564
- throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
5565
- }
5566
- }
5567
- }
5568
- /**
5569
- * Render source content with Markdown and Jinja syntax highlighting.
5570
- * Used for Source tab content.
5571
- * @public Exported for testing.
5572
- */
5573
- function renderSourceContent(content) {
5574
- return `<pre>${content.split("\n").map((line) => highlightSourceLine(line)).join("\n")}</pre>`;
5575
- }
5576
- /**
5577
- * Highlight a single line of source code (Markdown + Jinja).
5578
- */
5579
- function highlightSourceLine(line) {
5580
- let result = escapeHtml(line);
5581
- result = result.replace(/(\{%\s*)([a-zA-Z_/]+)(\s+[^%]*)?(%\})/g, (_, open, keyword, attrs, close) => {
5582
- let attrHtml = "";
5583
- if (attrs) attrHtml = attrs.replace(/([a-zA-Z_]+)(=)("[^"]*"|&#039;[^&#]*&#039;|[^\s%]+)?/g, (_m, attrName, eq, attrValue) => {
5584
- return `<span class="syn-jinja-attr">${attrName}</span>${eq}${attrValue ? `<span class="syn-jinja-value">${attrValue}</span>` : ""}`;
5585
- });
5586
- return `<span class="syn-jinja-tag">${open}</span><span class="syn-jinja-keyword">${keyword}</span>${attrHtml}<span class="syn-jinja-tag">${close}</span>`;
5587
- });
5588
- result = result.replace(/(\{#)(.*?)(#\})/g, `<span class="syn-comment">$1$2$3</span>`);
5589
- result = result.replace(/^(#{1,6}\s.*)$/gm, "<span class=\"syn-md-header\">$1</span>");
5590
- if (result === "---") result = "<span class=\"syn-comment\">---</span>";
5591
- return result;
5592
- }
5593
- /**
5594
- * Render markdown content (content only, no page wrapper).
5595
- * Used for tab content.
5596
- * @public Exported for testing.
5597
- */
5598
- function renderMarkdownContent(content) {
5599
- const lines = content.split("\n");
5600
- let html = "<div class=\"markdown-content\">";
5601
- let inParagraph = false;
5602
- let inCodeBlock = false;
5603
- let codeBlockContent = "";
5604
- let inUnorderedList = false;
5605
- let inOrderedList = false;
5606
- let inTable = false;
5607
- let tableHeaderDone = false;
5608
- const closeList = () => {
5609
- if (inUnorderedList) {
5610
- html += "</ul>";
5611
- inUnorderedList = false;
5612
- }
5613
- if (inOrderedList) {
5614
- html += "</ol>";
5615
- inOrderedList = false;
5616
- }
5617
- };
5618
- const closeTable = () => {
5619
- if (inTable) {
5620
- html += "</tbody></table></div>";
5621
- inTable = false;
5622
- tableHeaderDone = false;
5623
- }
5624
- };
5625
- const isTableRow = (line) => {
5626
- const trimmed = line.trim();
5627
- return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
5628
- };
5629
- const isTableSeparator = (line) => {
5630
- const trimmed = line.trim();
5631
- return /^\|[\s-:|]+\|$/.test(trimmed);
5632
- };
5633
- const parseTableCells = (line) => {
5634
- return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
5635
- };
5636
- for (const line of lines) {
5637
- const trimmed = line.trim();
5638
- if (trimmed.startsWith("```")) {
5639
- if (inCodeBlock) {
5640
- html += `<pre><code>${escapeHtml(codeBlockContent.trim())}</code></pre>`;
5641
- codeBlockContent = "";
5642
- inCodeBlock = false;
5643
- } else {
5644
- if (inParagraph) {
5645
- html += "</p>";
5646
- inParagraph = false;
5647
- }
5648
- closeList();
5649
- closeTable();
5650
- inCodeBlock = true;
5651
- }
5652
- continue;
5653
- }
5654
- if (inCodeBlock) {
5655
- codeBlockContent += line + "\n";
5656
- continue;
5657
- }
5658
- if (isTableRow(trimmed)) {
5659
- if (inParagraph) {
5660
- html += "</p>";
5661
- inParagraph = false;
5662
- }
5663
- closeList();
5664
- if (isTableSeparator(trimmed)) {
5665
- tableHeaderDone = true;
5666
- continue;
5667
- }
5668
- const cells = parseTableCells(trimmed);
5669
- if (!inTable) {
5670
- html += "<div class=\"table-container\"><table class=\"data-table\"><thead><tr>";
5671
- for (const cell of cells) html += `<th>${formatInlineMarkdown(cell)}</th>`;
5672
- html += "</tr></thead><tbody>";
5673
- inTable = true;
5674
- } else if (tableHeaderDone) {
5675
- html += "<tr>";
5676
- for (const cell of cells) html += `<td>${formatInlineMarkdown(cell)}</td>`;
5677
- html += "</tr>";
5678
- }
5679
- continue;
5680
- }
5681
- if (inTable && !isTableRow(trimmed)) closeTable();
5682
- if (trimmed.startsWith("# ")) {
5683
- if (inParagraph) {
5684
- html += "</p>";
5685
- inParagraph = false;
5686
- }
5687
- closeList();
5688
- html += `<h2>${formatInlineMarkdown(trimmed.slice(2))}</h2>`;
5689
- } else if (trimmed.startsWith("## ")) {
5690
- if (inParagraph) {
5691
- html += "</p>";
5692
- inParagraph = false;
5693
- }
5694
- closeList();
5695
- html += `<h3>${formatInlineMarkdown(trimmed.slice(3))}</h3>`;
5696
- } else if (trimmed.startsWith("### ")) {
5697
- if (inParagraph) {
5698
- html += "</p>";
5699
- inParagraph = false;
5700
- }
5701
- closeList();
5702
- html += `<h4>${formatInlineMarkdown(trimmed.slice(4))}</h4>`;
5703
- } else if (trimmed.startsWith("#### ")) {
5704
- if (inParagraph) {
5705
- html += "</p>";
5706
- inParagraph = false;
5707
- }
5708
- closeList();
5709
- html += `<h5>${formatInlineMarkdown(trimmed.slice(5))}</h5>`;
5710
- } else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
5711
- if (inParagraph) {
5712
- html += "</p>";
5713
- inParagraph = false;
5714
- }
5715
- if (inOrderedList) {
5716
- html += "</ol>";
5717
- inOrderedList = false;
5718
- }
5719
- if (!inUnorderedList) {
5720
- html += "<ul>";
5721
- inUnorderedList = true;
5722
- }
5723
- const itemContent = trimmed.slice(2);
5724
- const liClass = /^\[[ xX]\]/.test(itemContent) ? " class=\"checkbox-item\"" : "";
5725
- html += `<li${liClass}>${formatInlineMarkdown(itemContent)}</li>`;
5726
- } else if (/^\d+\.\s/.test(trimmed)) {
5727
- if (inParagraph) {
5728
- html += "</p>";
5729
- inParagraph = false;
5730
- }
5731
- if (inUnorderedList) {
5732
- html += "</ul>";
5733
- inUnorderedList = false;
5734
- }
5735
- if (!inOrderedList) {
5736
- html += "<ol>";
5737
- inOrderedList = true;
5738
- }
5739
- const text = trimmed.replace(/^\d+\.\s/, "");
5740
- html += `<li>${formatInlineMarkdown(text)}</li>`;
5741
- } else if (trimmed === "") {
5742
- if (inParagraph) {
5743
- html += "</p>";
5744
- inParagraph = false;
5745
- }
5746
- closeList();
5747
- } else {
5748
- closeList();
5749
- if (!inParagraph) {
5750
- html += "<p>";
5751
- inParagraph = true;
5752
- } else html += "<br>";
5753
- html += formatInlineMarkdown(trimmed);
5754
- }
5755
- }
5756
- if (inParagraph) html += "</p>";
5757
- closeList();
5758
- closeTable();
5759
- html += "</div>";
5760
- return html;
5761
- }
5762
- /**
5763
- * Format inline markdown (bold, italic, code, links, checkboxes).
5764
- * Also auto-links bare URLs for consistency.
5765
- */
5766
- function formatInlineMarkdown(text) {
5767
- let result = escapeHtml(text);
5768
- result = result.replace(/\[x\]/gi, "<span class=\"checkbox checked\">☑</span>");
5769
- result = result.replace(/\[ \]/g, "<span class=\"checkbox unchecked\">☐</span>");
5770
- result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
5771
- result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
5772
- result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
5773
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => {
5774
- const cleanUrl = url.replace(/&amp;/g, "&");
5775
- return `<a href="${cleanUrl}" target="_blank" class="url-link" data-url="${cleanUrl}">${linkText}</a>`;
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
- });
5783
- return result;
5784
- }
5785
- /**
5786
- * Render YAML content (content only, no page wrapper).
5787
- * Used for tab content.
5788
- * @public Exported for testing.
5789
- */
5790
- function renderYamlContent(content) {
5791
- return `<pre>${content.split("\n").map((line) => {
5792
- if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
5793
- const colonIndex = line.indexOf(":");
5794
- if (colonIndex > 0 && !line.trim().startsWith("-")) {
5795
- const key = escapeHtml(line.slice(0, colonIndex));
5796
- const afterColon = line.slice(colonIndex + 1).trim();
5797
- const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
5798
- if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
5799
- const valueStart = line.indexOf(afterColon, colonIndex);
5800
- return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
5801
- }
5802
- if (line.trim().startsWith("-")) {
5803
- const dashIndex = line.indexOf("-");
5804
- const beforeDash = escapeHtml(line.slice(0, dashIndex));
5805
- const afterDash = line.slice(dashIndex + 1).trim();
5806
- if (afterDash === "") return `${beforeDash}-`;
5807
- return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
5808
- }
5809
- return escapeHtml(line);
5810
- }).join("\n")}</pre>`;
5811
- }
5812
- /**
5813
- * Render JSON content (content only, no page wrapper).
5814
- * Used for tab content.
5815
- * @public Exported for testing.
5816
- */
5817
- function renderJsonContent(content) {
5818
- let formatted;
5819
- try {
5820
- const parsed = JSON.parse(content);
5821
- formatted = JSON.stringify(parsed, null, 2);
5822
- } catch {
5823
- formatted = content;
5824
- }
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>`;
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
- }
6610
5487
 
6611
5488
  //#endregion
6612
5489
  //#region src/cli/commands/render.ts
@@ -6801,9 +5678,9 @@ function registerResearchCommand(program) {
6801
5678
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
6802
5679
  console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
6803
5680
  if (options.transcript && result.transcript) {
6804
- const { serializeSession } = await import("./session-ZHBi3LVQ.mjs");
5681
+ const { serializeSession } = await import("./session-VeSkVrck.mjs");
6805
5682
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
6806
- const { writeFile } = await import("./shared-BTR35aMz.mjs");
5683
+ const { writeFile } = await import("./shared-fb0nkzQi.mjs");
6807
5684
  await writeFile(transcriptPath, serializeSession(result.transcript));
6808
5685
  logInfo(ctx, `Transcript: ${transcriptPath}`);
6809
5686
  }
@@ -7208,4 +6085,4 @@ async function runCli() {
7208
6085
 
7209
6086
  //#endregion
7210
6087
  export { runCli as t };
7211
- //# sourceMappingURL=cli-FFMoEhFS.mjs.map
6088
+ //# sourceMappingURL=cli-C8F9yDsv.mjs.map