markform 0.1.21 → 0.1.23

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 (33) hide show
  1. package/README.md +66 -18
  2. package/dist/ai-sdk.d.mts +1 -1
  3. package/dist/ai-sdk.mjs +1 -1
  4. package/dist/{apply-CD-t7ovb.mjs → apply-KzQztrDV.mjs} +100 -74
  5. package/dist/apply-KzQztrDV.mjs.map +1 -0
  6. package/dist/bin.mjs +1 -1
  7. package/dist/{cli-ChdIy1a7.mjs → cli-ZcOC47KK.mjs} +24 -1213
  8. package/dist/cli-ZcOC47KK.mjs.map +1 -0
  9. package/dist/cli.mjs +1 -1
  10. package/dist/{coreTypes-BQrWf_Wt.d.mts → coreTypes-BlsJkU1w.d.mts} +1 -1
  11. package/dist/fillRecord-DTl5lnK0.d.mts +345 -0
  12. package/dist/fillRecordRenderer-VBQ2vwPV.mjs +1253 -0
  13. package/dist/fillRecordRenderer-VBQ2vwPV.mjs.map +1 -0
  14. package/dist/index.d.mts +53 -343
  15. package/dist/index.mjs +4 -4
  16. package/dist/render.d.mts +74 -0
  17. package/dist/render.mjs +4 -0
  18. package/dist/{session-ZgegwtkT.mjs → session-BCcltrLA.mjs} +1 -1
  19. package/dist/{session-ZgegwtkT.mjs.map → session-BCcltrLA.mjs.map} +1 -1
  20. package/dist/{session-BPuQ-ok0.mjs → session-VeSkVrck.mjs} +1 -1
  21. package/dist/{shared-DwdyWmvE.mjs → shared-CsdT2T7k.mjs} +1 -1
  22. package/dist/{shared-DwdyWmvE.mjs.map → shared-CsdT2T7k.mjs.map} +1 -1
  23. package/dist/{shared-BTR35aMz.mjs → shared-fb0nkzQi.mjs} +1 -1
  24. package/dist/{src-DOPe4tmu.mjs → src-B2uFvGli.mjs} +103 -21
  25. package/dist/{src-DOPe4tmu.mjs.map → src-B2uFvGli.mjs.map} +1 -1
  26. package/dist/urlFormat-lls7CsEP.mjs +71 -0
  27. package/dist/urlFormat-lls7CsEP.mjs.map +1 -0
  28. package/docs/markform-apis.md +53 -0
  29. package/examples/simple/simple-skipped-filled.report.md +8 -8
  30. package/examples/twitter-thread/twitter-thread.form.md +373 -0
  31. package/package.json +5 -1
  32. package/dist/apply-CD-t7ovb.mjs.map +0 -1
  33. package/dist/cli-ChdIy1a7.mjs.map +0 -1
@@ -1,9 +1,10 @@
1
1
 
2
2
  import { R as PatchSchema } from "./coreTypes-CTLr-NGd.mjs";
3
- import { 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, at as parseModelIdForDisplay, c as computeProgressSummary, d as serializeForm, et as SUGGESTED_LLMS, f as serializeRawMarkdown, h as friendlyUrlAbbrev, i as inspect, it as hasWebSearchSupport, j as AGENT_ROLE, l as computeStructureSummary, m as formatBareUrlsAsHtmlLinks, n as getAllFields, nt as formatSuggestedLlms, p as serializeReport, q as deriveExportPath, t as applyPatches, tt as WEB_SEARCH_CONFIG, v as validateSyntaxConsistency } from "./apply-CD-t7ovb.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-DOPe4tmu.mjs";
5
- import { n as serializeSession } from "./session-ZgegwtkT.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";
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-KzQztrDV.mjs";
4
+ import { A as createMockAgent, C as getProviderNames, D as FillRecordCollector, E as createLiveAgent, M as createHarness, O as computeExecutionPlan, S as getProviderInfo, T as buildMockWireFormat, U as formToJsonSchema, W as parseForm, _ as fillForm, g as resolveHarnessConfig, h as formatFillRecordSummary, i as runResearch, m as stripUnstableFillRecordFields, n as isResearchForm, t as VERSION, w as resolveModel } from "./src-B2uFvGli.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-VBQ2vwPV.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)}`);
@@ -3091,7 +3092,7 @@ function registerFillCommand(program) {
3091
3092
  logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
3092
3093
  for (const patch of patches) {
3093
3094
  const typeName = formatPatchType(patch);
3094
- const value = formatPatchValue$1(patch);
3095
+ const value = formatPatchValue(patch);
3095
3096
  const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
3096
3097
  if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
3097
3098
  else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
@@ -4676,6 +4677,12 @@ function renderFormHtml(form, tabs) {
4676
4677
  border-radius: 3px;
4677
4678
  margin-left: 0.5rem;
4678
4679
  }
4680
+ .skip-reason {
4681
+ font-size: 0.85rem;
4682
+ color: #6c757d;
4683
+ font-style: italic;
4684
+ margin-top: 0.25rem;
4685
+ }
4679
4686
  .table-container {
4680
4687
  overflow-x: auto;
4681
4688
  }
@@ -5033,7 +5040,9 @@ function renderGroup(group, responses) {
5033
5040
  const groupTitle = group.title ?? group.id;
5034
5041
  const fieldsHtml = group.children.map((field) => {
5035
5042
  const response = responses[field.id];
5036
- return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
5043
+ const value = response?.state === "answered" ? response.value : void 0;
5044
+ const isSkipped = response?.state === "skipped";
5045
+ return renderFieldHtml(field, value, isSkipped, isSkipped ? response?.reason : void 0);
5037
5046
  }).join("\n");
5038
5047
  return `
5039
5048
  <div class="group">
@@ -5045,11 +5054,12 @@ function renderGroup(group, responses) {
5045
5054
  * Render a field as HTML.
5046
5055
  * @public Exported for testing.
5047
5056
  */
5048
- function renderFieldHtml(field, value, isSkipped) {
5057
+ function renderFieldHtml(field, value, isSkipped, skipReason) {
5049
5058
  const skipped = isSkipped === true;
5050
5059
  const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
5051
5060
  const typeLabel = `<span class="type-badge">${field.kind}</span>`;
5052
- const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
5061
+ const skippedBadge = skipped ? `<span class="skipped-badge">Skipped</span>` : "";
5062
+ const skipReasonHtml = skipped && skipReason ? `<div class="skip-reason">(skipped: ${escapeHtml(skipReason)})</div>` : "";
5053
5063
  const fieldClass = skipped ? "field field-skipped" : "field";
5054
5064
  const disabledAttr = skipped ? " disabled" : "";
5055
5065
  let inputHtml;
@@ -5101,6 +5111,7 @@ function renderFieldHtml(field, value, isSkipped) {
5101
5111
  ${escapeHtml(field.label)} ${requiredMark} ${typeLabel} ${skippedBadge}
5102
5112
  </label>
5103
5113
  ${inputHtml}
5114
+ ${skipReasonHtml}
5104
5115
  ${skipButton}
5105
5116
  </div>`;
5106
5117
  }
@@ -5282,13 +5293,6 @@ function renderTableInput(field, value, _disabledAttr) {
5282
5293
  <div class="field-help">(table fields are currently read-only in the web UI)</div>
5283
5294
  </div>`;
5284
5295
  }
5285
- /**
5286
- * Escape HTML special characters.
5287
- * @public Exported for testing.
5288
- */
5289
- function escapeHtml(str) {
5290
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
5291
- }
5292
5296
  /** Common styles for read-only viewers */
5293
5297
  const READ_ONLY_STYLES = `
5294
5298
  * { box-sizing: border-box; }
@@ -5440,17 +5444,6 @@ function renderYamlHtml(content, filename) {
5440
5444
  </html>`;
5441
5445
  }
5442
5446
  /**
5443
- * Highlight a YAML value based on its type.
5444
- */
5445
- function highlightYamlValue(value) {
5446
- const trimmed = value.trim();
5447
- if (trimmed === "true" || trimmed === "false") return `<span class="syn-bool">${escapeHtml(value)}</span>`;
5448
- if (trimmed === "null" || trimmed === "~") return `<span class="syn-null">${escapeHtml(value)}</span>`;
5449
- if (/^-?\d+\.?\d*$/.test(trimmed)) return `<span class="syn-number">${escapeHtml(value)}</span>`;
5450
- if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return `<span class="syn-string">${escapeHtml(value)}</span>`;
5451
- return `<span class="syn-string">${escapeHtml(value)}</span>`;
5452
- }
5453
- /**
5454
5447
  * Render JSON content with syntax highlighting and formatting.
5455
5448
  */
5456
5449
  function renderJsonHtml(content, filename) {
@@ -5498,1188 +5491,6 @@ function renderPlainTextHtml(content, filename) {
5498
5491
  </body>
5499
5492
  </html>`;
5500
5493
  }
5501
- /**
5502
- * Render form view content (read-only display of form fields).
5503
- * Used for View tab content.
5504
- * @public Exported for testing.
5505
- */
5506
- function renderViewContent(form) {
5507
- const { schema, responsesByFieldId } = form;
5508
- let html = "<div class=\"view-content\">";
5509
- for (const group of schema.groups) {
5510
- const groupTitle = group.title ?? group.id;
5511
- html += `<div class="view-group"><h2>${escapeHtml(groupTitle)}</h2>`;
5512
- for (const field of group.children) {
5513
- const response = responsesByFieldId[field.id];
5514
- const value = response?.state === "answered" ? response.value : void 0;
5515
- const isSkipped = response?.state === "skipped";
5516
- html += "<div class=\"view-field\">";
5517
- html += `<div class="view-field-label">${escapeHtml(field.label)}`;
5518
- html += ` <span class="type-badge">${field.kind}</span>`;
5519
- if (field.required) html += " <span class=\"required\">*</span>";
5520
- if (isSkipped) html += " <span class=\"skipped-badge\">Skipped</span>";
5521
- html += "</div>";
5522
- html += renderViewFieldValue(field, value, isSkipped);
5523
- html += "</div>";
5524
- }
5525
- html += "</div>";
5526
- }
5527
- html += "</div>";
5528
- return html;
5529
- }
5530
- /**
5531
- * Format a checkbox state for display.
5532
- */
5533
- function formatCheckboxState(state) {
5534
- switch (state) {
5535
- case "done": return "<span class=\"checkbox checked\">☑</span>";
5536
- case "todo": return "<span class=\"checkbox unchecked\">☐</span>";
5537
- case "active": return "<span class=\"state-badge state-active\">●</span>";
5538
- case "incomplete": return "<span class=\"state-badge state-incomplete\">○</span>";
5539
- case "na": return "<span class=\"state-badge state-na\">—</span>";
5540
- case "yes": return "<span class=\"checkbox checked\">☑</span>";
5541
- case "no": return "<span class=\"checkbox unchecked\">☐</span>";
5542
- case "unfilled": return "<span class=\"state-badge state-unfilled\">?</span>";
5543
- default: return `<span class="state-badge">${escapeHtml(state)}</span>`;
5544
- }
5545
- }
5546
- /**
5547
- * Render a field value for the View tab.
5548
- */
5549
- function renderViewFieldValue(field, value, isSkipped) {
5550
- if (isSkipped) return "<div class=\"view-field-empty\">(skipped)</div>";
5551
- if (value === void 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5552
- switch (field.kind) {
5553
- case "string": {
5554
- const v = value.kind === "string" ? value.value : null;
5555
- if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5556
- return `<div class="view-field-value">${formatBareUrlsAsHtmlLinks(v, escapeHtml)}</div>`;
5557
- }
5558
- case "number": {
5559
- const v = value.kind === "number" ? value.value : null;
5560
- if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
5561
- return `<div class="view-field-value">${v}</div>`;
5562
- }
5563
- case "string_list": {
5564
- const items = value.kind === "string_list" ? value.items : [];
5565
- if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5566
- return `<div class="view-field-value"><ul>${items.map((i) => `<li>${formatBareUrlsAsHtmlLinks(i, escapeHtml)}</li>`).join("")}</ul></div>`;
5567
- }
5568
- case "single_select": {
5569
- const selected = value.kind === "single_select" ? value.selected : null;
5570
- if (selected === null) return "<div class=\"view-field-empty\">(not filled)</div>";
5571
- return `<div class="view-field-value">${escapeHtml(field.options.find((o) => o.id === selected)?.label ?? selected)}</div>`;
5572
- }
5573
- case "multi_select": {
5574
- const selected = value.kind === "multi_select" ? value.selected : [];
5575
- return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
5576
- return `<li class="checkbox-item">${selected.includes(opt.id) ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
5577
- }).join("")}</ul></div>`;
5578
- }
5579
- case "checkboxes": {
5580
- const values = value.kind === "checkboxes" ? value.values : {};
5581
- const mode = field.checkboxMode ?? "multi";
5582
- return `<div class="view-field-value"><ul class="checkbox-list">${field.options.map((opt) => {
5583
- const state = values[opt.id] ?? (mode === "explicit" ? "unfilled" : "todo");
5584
- if (mode === "simple") return `<li class="checkbox-item">${state === "done" ? "<span class=\"checkbox checked\">☑</span>" : "<span class=\"checkbox unchecked\">☐</span>"} ${escapeHtml(opt.label)}</li>`;
5585
- return `<li class="checkbox-item">${formatCheckboxState(state)} ${escapeHtml(opt.label)}</li>`;
5586
- }).join("")}</ul></div>`;
5587
- }
5588
- case "url": {
5589
- const v = value.kind === "url" ? value.value : null;
5590
- if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5591
- const domain = friendlyUrlAbbrev(v);
5592
- return `<div class="view-field-value"><a href="${escapeHtml(v)}" target="_blank" class="url-link" data-url="${escapeHtml(v)}">${escapeHtml(domain)}</a></div>`;
5593
- }
5594
- case "url_list": {
5595
- const items = value.kind === "url_list" ? value.items : [];
5596
- if (items.length === 0) return "<div class=\"view-field-empty\">(not filled)</div>";
5597
- return `<div class="view-field-value"><ul>${items.map((u) => {
5598
- const domain = friendlyUrlAbbrev(u);
5599
- return `<li><a href="${escapeHtml(u)}" target="_blank" class="url-link" data-url="${escapeHtml(u)}">${escapeHtml(domain)}</a></li>`;
5600
- }).join("")}</ul></div>`;
5601
- }
5602
- case "date": {
5603
- const v = value.kind === "date" ? value.value : null;
5604
- if (v === null || v === "") return "<div class=\"view-field-empty\">(not filled)</div>";
5605
- return `<div class="view-field-value">${escapeHtml(v)}</div>`;
5606
- }
5607
- case "year": {
5608
- const v = value.kind === "year" ? value.value : null;
5609
- if (v === null) return "<div class=\"view-field-empty\">(not filled)</div>";
5610
- return `<div class="view-field-value">${v}</div>`;
5611
- }
5612
- case "table": {
5613
- const rows = value.kind === "table" ? value.rows : [];
5614
- if (rows.length === 0) return "<div class=\"view-field-empty\">(no data)</div>";
5615
- let tableHtml = "<div class=\"table-container\"><table class=\"data-table\">";
5616
- tableHtml += "<thead><tr>";
5617
- for (const col of field.columns) tableHtml += `<th>${escapeHtml(col.label)}</th>`;
5618
- tableHtml += "</tr></thead><tbody>";
5619
- for (const row of rows) {
5620
- tableHtml += "<tr>";
5621
- for (const col of field.columns) {
5622
- const cell = row[col.id];
5623
- let cellValue = "";
5624
- let cellHtml = "";
5625
- if (cell?.state === "answered" && cell.value !== void 0 && cell.value !== null) {
5626
- cellValue = String(cell.value);
5627
- if (col.type === "url" && cellValue) {
5628
- const domain = friendlyUrlAbbrev(cellValue);
5629
- cellHtml = `<a href="${escapeHtml(cellValue)}" target="_blank" class="url-link" data-url="${escapeHtml(cellValue)}">${escapeHtml(domain)}</a>`;
5630
- } else cellHtml = formatBareUrlsAsHtmlLinks(cellValue, escapeHtml);
5631
- }
5632
- tableHtml += `<td>${cellHtml}</td>`;
5633
- }
5634
- tableHtml += "</tr>";
5635
- }
5636
- tableHtml += "</tbody></table></div>";
5637
- return tableHtml;
5638
- }
5639
- default: {
5640
- const _exhaustive = field;
5641
- throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
5642
- }
5643
- }
5644
- }
5645
- /**
5646
- * Render source content with Markdown and Jinja syntax highlighting.
5647
- * Used for Source tab content.
5648
- * @public Exported for testing.
5649
- */
5650
- function renderSourceContent(content) {
5651
- return `<pre>${content.split("\n").map((line) => highlightSourceLine(line)).join("\n")}</pre>`;
5652
- }
5653
- /**
5654
- * Highlight a single line of source code (Markdown + Jinja).
5655
- */
5656
- function highlightSourceLine(line) {
5657
- let result = escapeHtml(line);
5658
- result = result.replace(/(\{%\s*)([a-zA-Z_/]+)(\s+[^%]*)?(%\})/g, (_, open, keyword, attrs, close) => {
5659
- let attrHtml = "";
5660
- if (attrs) attrHtml = attrs.replace(/([a-zA-Z_]+)(=)("[^"]*"|&#039;[^&#]*&#039;|[^\s%]+)?/g, (_m, attrName, eq, attrValue) => {
5661
- return `<span class="syn-jinja-attr">${attrName}</span>${eq}${attrValue ? `<span class="syn-jinja-value">${attrValue}</span>` : ""}`;
5662
- });
5663
- return `<span class="syn-jinja-tag">${open}</span><span class="syn-jinja-keyword">${keyword}</span>${attrHtml}<span class="syn-jinja-tag">${close}</span>`;
5664
- });
5665
- result = result.replace(/(\{#)(.*?)(#\})/g, `<span class="syn-comment">$1$2$3</span>`);
5666
- result = result.replace(/^(#{1,6}\s.*)$/gm, "<span class=\"syn-md-header\">$1</span>");
5667
- if (result === "---") result = "<span class=\"syn-comment\">---</span>";
5668
- return result;
5669
- }
5670
- /**
5671
- * Render markdown content (content only, no page wrapper).
5672
- * Used for tab content.
5673
- * @public Exported for testing.
5674
- */
5675
- function renderMarkdownContent(content) {
5676
- const lines = content.split("\n");
5677
- let html = "<div class=\"markdown-content\">";
5678
- let inParagraph = false;
5679
- let inCodeBlock = false;
5680
- let codeBlockContent = "";
5681
- let inUnorderedList = false;
5682
- let inOrderedList = false;
5683
- let inTable = false;
5684
- let tableHeaderDone = false;
5685
- const closeList = () => {
5686
- if (inUnorderedList) {
5687
- html += "</ul>";
5688
- inUnorderedList = false;
5689
- }
5690
- if (inOrderedList) {
5691
- html += "</ol>";
5692
- inOrderedList = false;
5693
- }
5694
- };
5695
- const closeTable = () => {
5696
- if (inTable) {
5697
- html += "</tbody></table></div>";
5698
- inTable = false;
5699
- tableHeaderDone = false;
5700
- }
5701
- };
5702
- const isTableRow = (line) => {
5703
- const trimmed = line.trim();
5704
- return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
5705
- };
5706
- const isTableSeparator = (line) => {
5707
- const trimmed = line.trim();
5708
- return /^\|[\s-:|]+\|$/.test(trimmed);
5709
- };
5710
- const parseTableCells = (line) => {
5711
- return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
5712
- };
5713
- for (const line of lines) {
5714
- const trimmed = line.trim();
5715
- if (trimmed.startsWith("```")) {
5716
- if (inCodeBlock) {
5717
- html += `<pre><code>${escapeHtml(codeBlockContent.trim())}</code></pre>`;
5718
- codeBlockContent = "";
5719
- inCodeBlock = false;
5720
- } else {
5721
- if (inParagraph) {
5722
- html += "</p>";
5723
- inParagraph = false;
5724
- }
5725
- closeList();
5726
- closeTable();
5727
- inCodeBlock = true;
5728
- }
5729
- continue;
5730
- }
5731
- if (inCodeBlock) {
5732
- codeBlockContent += line + "\n";
5733
- continue;
5734
- }
5735
- if (isTableRow(trimmed)) {
5736
- if (inParagraph) {
5737
- html += "</p>";
5738
- inParagraph = false;
5739
- }
5740
- closeList();
5741
- if (isTableSeparator(trimmed)) {
5742
- tableHeaderDone = true;
5743
- continue;
5744
- }
5745
- const cells = parseTableCells(trimmed);
5746
- if (!inTable) {
5747
- html += "<div class=\"table-container\"><table class=\"data-table\"><thead><tr>";
5748
- for (const cell of cells) html += `<th>${formatInlineMarkdown(cell)}</th>`;
5749
- html += "</tr></thead><tbody>";
5750
- inTable = true;
5751
- } else if (tableHeaderDone) {
5752
- html += "<tr>";
5753
- for (const cell of cells) html += `<td>${formatInlineMarkdown(cell)}</td>`;
5754
- html += "</tr>";
5755
- }
5756
- continue;
5757
- }
5758
- if (inTable && !isTableRow(trimmed)) closeTable();
5759
- if (trimmed.startsWith("# ")) {
5760
- if (inParagraph) {
5761
- html += "</p>";
5762
- inParagraph = false;
5763
- }
5764
- closeList();
5765
- html += `<h2>${formatInlineMarkdown(trimmed.slice(2))}</h2>`;
5766
- } else if (trimmed.startsWith("## ")) {
5767
- if (inParagraph) {
5768
- html += "</p>";
5769
- inParagraph = false;
5770
- }
5771
- closeList();
5772
- html += `<h3>${formatInlineMarkdown(trimmed.slice(3))}</h3>`;
5773
- } else if (trimmed.startsWith("### ")) {
5774
- if (inParagraph) {
5775
- html += "</p>";
5776
- inParagraph = false;
5777
- }
5778
- closeList();
5779
- html += `<h4>${formatInlineMarkdown(trimmed.slice(4))}</h4>`;
5780
- } else if (trimmed.startsWith("#### ")) {
5781
- if (inParagraph) {
5782
- html += "</p>";
5783
- inParagraph = false;
5784
- }
5785
- closeList();
5786
- html += `<h5>${formatInlineMarkdown(trimmed.slice(5))}</h5>`;
5787
- } else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
5788
- if (inParagraph) {
5789
- html += "</p>";
5790
- inParagraph = false;
5791
- }
5792
- if (inOrderedList) {
5793
- html += "</ol>";
5794
- inOrderedList = false;
5795
- }
5796
- if (!inUnorderedList) {
5797
- html += "<ul>";
5798
- inUnorderedList = true;
5799
- }
5800
- const itemContent = trimmed.slice(2);
5801
- const liClass = /^\[[ xX]\]/.test(itemContent) ? " class=\"checkbox-item\"" : "";
5802
- html += `<li${liClass}>${formatInlineMarkdown(itemContent)}</li>`;
5803
- } else if (/^\d+\.\s/.test(trimmed)) {
5804
- if (inParagraph) {
5805
- html += "</p>";
5806
- inParagraph = false;
5807
- }
5808
- if (inUnorderedList) {
5809
- html += "</ul>";
5810
- inUnorderedList = false;
5811
- }
5812
- if (!inOrderedList) {
5813
- html += "<ol>";
5814
- inOrderedList = true;
5815
- }
5816
- const text = trimmed.replace(/^\d+\.\s/, "");
5817
- html += `<li>${formatInlineMarkdown(text)}</li>`;
5818
- } else if (trimmed === "") {
5819
- if (inParagraph) {
5820
- html += "</p>";
5821
- inParagraph = false;
5822
- }
5823
- closeList();
5824
- } else {
5825
- closeList();
5826
- if (!inParagraph) {
5827
- html += "<p>";
5828
- inParagraph = true;
5829
- } else html += "<br>";
5830
- html += formatInlineMarkdown(trimmed);
5831
- }
5832
- }
5833
- if (inParagraph) html += "</p>";
5834
- closeList();
5835
- closeTable();
5836
- html += "</div>";
5837
- return html;
5838
- }
5839
- /**
5840
- * Format inline markdown (bold, italic, code, links, checkboxes).
5841
- * Also auto-links bare URLs for consistency.
5842
- */
5843
- function formatInlineMarkdown(text) {
5844
- let result = escapeHtml(text);
5845
- result = result.replace(/\[x\]/gi, "<span class=\"checkbox checked\">☑</span>");
5846
- result = result.replace(/\[ \]/g, "<span class=\"checkbox unchecked\">☐</span>");
5847
- result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
5848
- result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
5849
- result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
5850
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => {
5851
- const cleanUrl = url.replace(/&amp;/g, "&");
5852
- return `<a href="${cleanUrl}" target="_blank" class="url-link" data-url="${cleanUrl}">${linkText}</a>`;
5853
- });
5854
- result = result.replace(/(?<!href="|data-url="|">|\]\()(?:https?:\/\/|www\.)[^\s<>"]+(?<![.,;:!?'")])/g, (url) => {
5855
- const cleanUrl = url.replace(/&amp;/g, "&");
5856
- const fullUrl = cleanUrl.startsWith("www.") ? `https://${cleanUrl}` : cleanUrl;
5857
- const display = friendlyUrlAbbrev(fullUrl);
5858
- return `<a href="${escapeHtml(fullUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(fullUrl)}">${escapeHtml(display)}</a>`;
5859
- });
5860
- return result;
5861
- }
5862
- /**
5863
- * Render YAML content (content only, no page wrapper).
5864
- * Used for tab content.
5865
- * @public Exported for testing.
5866
- */
5867
- function renderYamlContent(content) {
5868
- return `<pre>${content.split("\n").map((line) => {
5869
- if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
5870
- const colonIndex = line.indexOf(":");
5871
- if (colonIndex > 0 && !line.trim().startsWith("-")) {
5872
- const key = escapeHtml(line.slice(0, colonIndex));
5873
- const afterColon = line.slice(colonIndex + 1).trim();
5874
- const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
5875
- if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
5876
- const valueStart = line.indexOf(afterColon, colonIndex);
5877
- return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
5878
- }
5879
- if (line.trim().startsWith("-")) {
5880
- const dashIndex = line.indexOf("-");
5881
- const beforeDash = escapeHtml(line.slice(0, dashIndex));
5882
- const afterDash = line.slice(dashIndex + 1).trim();
5883
- if (afterDash === "") return `${beforeDash}-`;
5884
- return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
5885
- }
5886
- return escapeHtml(line);
5887
- }).join("\n")}</pre>`;
5888
- }
5889
- /**
5890
- * Render JSON content (content only, no page wrapper).
5891
- * Used for tab content.
5892
- * @public Exported for testing.
5893
- */
5894
- function renderJsonContent(content) {
5895
- let formatted;
5896
- try {
5897
- const parsed = JSON.parse(content);
5898
- formatted = JSON.stringify(parsed, null, 2);
5899
- } catch {
5900
- formatted = content;
5901
- }
5902
- 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>`;
5903
- }
5904
- /**
5905
- * Format milliseconds as human-readable duration.
5906
- * @public Exported for reuse in other visualizations.
5907
- */
5908
- function formatDuration(ms) {
5909
- if (ms < 1e3) return `${ms.toFixed(0)}ms`;
5910
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
5911
- return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(0)}s`;
5912
- }
5913
- /**
5914
- * Format token count with K suffix for large numbers.
5915
- * @public Exported for reuse in other visualizations.
5916
- */
5917
- function formatTokens(count) {
5918
- if (count >= 1e4) return `${(count / 1e3).toFixed(1)}k`;
5919
- if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
5920
- return count.toLocaleString();
5921
- }
5922
- /**
5923
- * Format a patch value for display.
5924
- * Shows full content - the container has max-height with scroll for long values.
5925
- */
5926
- function formatPatchValue(value) {
5927
- if (value === null || value === void 0) return "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
5928
- if (typeof value === "string") return escapeHtml(value);
5929
- if (typeof value === "number" || typeof value === "boolean") return String(value);
5930
- return escapeHtml(JSON.stringify(value, null, 2));
5931
- }
5932
- /**
5933
- * Render patches from a fill_form tool call input.
5934
- * Returns HTML for the patch details section.
5935
- */
5936
- function renderPatchDetails(input) {
5937
- const patches = input.patches;
5938
- if (!Array.isArray(patches) || patches.length === 0) return "";
5939
- return `<div class="fr-turn__patches">${patches.map((patch) => {
5940
- if (!patch || typeof patch !== "object") return "";
5941
- const p = patch;
5942
- const op = typeof p.op === "string" ? p.op : "unknown";
5943
- const fieldId = typeof p.fieldId === "string" ? p.fieldId : typeof p.noteId === "string" ? p.noteId : "";
5944
- const opLabel = op.replace(/_/g, " ");
5945
- let valueHtml = "";
5946
- if (op === "skip_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(skipped)</em>";
5947
- else if (op === "abort_field") valueHtml = "<em class=\"fr-turn__patch-value--skip\">(aborted)</em>";
5948
- else if (op === "clear_field") valueHtml = "<em class=\"fr-turn__patch-value--clear\">(cleared)</em>";
5949
- else if ("value" in p) valueHtml = formatPatchValue(p.value);
5950
- else if ("values" in p) valueHtml = formatPatchValue(p.values);
5951
- else if ("rows" in p) valueHtml = formatPatchValue(p.rows);
5952
- return `
5953
- <div class="fr-turn__patch">
5954
- <span class="fr-turn__patch-field">${escapeHtml(fieldId)}</span>
5955
- <span class="fr-turn__patch-op">${escapeHtml(opLabel)}</span>
5956
- <span class="fr-turn__patch-value">${valueHtml}</span>
5957
- </div>
5958
- `;
5959
- }).filter(Boolean).join("")}</div>`;
5960
- }
5961
- /**
5962
- * Render a single tool call with enhanced details.
5963
- * Shows query for web_search, patch details for fill_form.
5964
- */
5965
- function renderToolCall(tc) {
5966
- const hasError = !!tc.result?.error;
5967
- const icon = tc.success ? "✓" : "✕";
5968
- const errorClass = hasError ? " fr-turn__tool--error" : "";
5969
- let resultSummary = "";
5970
- if (hasError) resultSummary = `Error: ${escapeHtml(tc.result?.error ?? "")}`;
5971
- else if (tc.result?.resultCount !== void 0) resultSummary = `${tc.result.resultCount} results`;
5972
- else resultSummary = "OK";
5973
- let detailHtml = "";
5974
- if (tc.tool === "web_search" && typeof tc.input.query === "string") detailHtml = ` <span class="fr-turn__query">"${escapeHtml(tc.input.query)}"</span>`;
5975
- const toolLine = `<li class="fr-turn__tool${errorClass}">${icon} <strong>${escapeHtml(tc.tool)}</strong>${detailHtml}: ${resultSummary} (${formatDuration(tc.durationMs)})</li>`;
5976
- if (tc.tool === "fill_form" && tc.input.patches) {
5977
- const patchDetails = renderPatchDetails(tc.input);
5978
- if (patchDetails) return toolLine + patchDetails;
5979
- }
5980
- return toolLine;
5981
- }
5982
- /**
5983
- * CSS styles for fill record visualization.
5984
- * Uses CSS custom properties for theming (supports dark mode via prefers-color-scheme).
5985
- * Designed to be lightweight, reusable, and embeddable.
5986
- */
5987
- const FILL_RECORD_STYLES = `
5988
- <style>
5989
- .fr-dashboard {
5990
- --fr-bg: #ffffff;
5991
- --fr-bg-muted: #f9fafb;
5992
- --fr-bg-subtle: #f3f4f6;
5993
- --fr-border: #e5e7eb;
5994
- --fr-text: #111827;
5995
- --fr-text-muted: #6b7280;
5996
- --fr-primary: #3b82f6;
5997
- --fr-success: #22c55e;
5998
- --fr-warning: #f59e0b;
5999
- --fr-error: #ef4444;
6000
- --fr-info: #6b7280;
6001
-
6002
- /* Typography - consolidated to fewer sizes */
6003
- --fr-font-sm: 13px;
6004
- --fr-font-base: 14px;
6005
- --fr-font-lg: 20px;
6006
-
6007
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
6008
- padding: 20px;
6009
- max-width: 900px;
6010
- margin: 0 auto;
6011
- color: var(--fr-text);
6012
- line-height: 1.5;
6013
- }
6014
-
6015
- @media (prefers-color-scheme: dark) {
6016
- .fr-dashboard {
6017
- --fr-bg: #1f2937;
6018
- --fr-bg-muted: #374151;
6019
- --fr-bg-subtle: #4b5563;
6020
- --fr-border: #4b5563;
6021
- --fr-text: #f9fafb;
6022
- --fr-text-muted: #9ca3af;
6023
- }
6024
- }
6025
-
6026
- .fr-header {
6027
- display: flex;
6028
- justify-content: space-between;
6029
- align-items: center;
6030
- margin-bottom: 16px;
6031
- padding-bottom: 12px;
6032
- border-bottom: 1px solid var(--fr-border);
6033
- }
6034
- .fr-header__model {
6035
- font-weight: 600;
6036
- font-size: var(--fr-font-base);
6037
- color: var(--fr-text);
6038
- }
6039
- .fr-header__time {
6040
- font-weight: 600;
6041
- font-size: var(--fr-font-base);
6042
- color: var(--fr-text);
6043
- }
6044
-
6045
- .fr-banner {
6046
- border-radius: 8px;
6047
- padding: 12px 16px;
6048
- margin-bottom: 20px;
6049
- font-size: var(--fr-font-base);
6050
- }
6051
- .fr-banner--error {
6052
- background: color-mix(in srgb, var(--fr-error) 10%, var(--fr-bg));
6053
- border: 1px solid var(--fr-error);
6054
- }
6055
- .fr-banner--warning {
6056
- background: color-mix(in srgb, var(--fr-warning) 10%, var(--fr-bg));
6057
- border: 1px solid var(--fr-warning);
6058
- }
6059
-
6060
- .fr-cards {
6061
- display: grid;
6062
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
6063
- gap: 16px;
6064
- margin-bottom: 24px;
6065
- }
6066
-
6067
- .fr-card {
6068
- padding: 16px;
6069
- background: var(--fr-bg-muted);
6070
- border-radius: 8px;
6071
- text-align: center;
6072
- }
6073
- .fr-card__label {
6074
- font-size: var(--fr-font-sm);
6075
- color: var(--fr-text-muted);
6076
- margin-bottom: 4px;
6077
- }
6078
- .fr-card__value {
6079
- font-size: var(--fr-font-lg);
6080
- font-weight: 600;
6081
- }
6082
- .fr-card__sub {
6083
- font-size: var(--fr-font-sm);
6084
- color: var(--fr-text-muted);
6085
- margin-top: 2px;
6086
- }
6087
-
6088
- .fr-badge {
6089
- display: inline-flex;
6090
- align-items: center;
6091
- gap: 4px;
6092
- padding: 4px 10px;
6093
- border-radius: 4px;
6094
- font-weight: 600;
6095
- font-size: var(--fr-font-sm);
6096
- }
6097
- .fr-badge--completed { background: color-mix(in srgb, var(--fr-success) 15%, transparent); color: var(--fr-success); }
6098
- .fr-badge--partial { background: color-mix(in srgb, var(--fr-warning) 15%, transparent); color: var(--fr-warning); }
6099
- .fr-badge--cancelled { background: color-mix(in srgb, var(--fr-info) 15%, transparent); color: var(--fr-info); }
6100
- .fr-badge--failed { background: color-mix(in srgb, var(--fr-error) 15%, transparent); color: var(--fr-error); }
6101
-
6102
- .fr-section {
6103
- margin-bottom: 24px;
6104
- }
6105
- .fr-section__title {
6106
- font-size: var(--fr-font-base);
6107
- font-weight: 500;
6108
- color: var(--fr-text);
6109
- margin-bottom: 8px;
6110
- }
6111
-
6112
- .fr-progress {
6113
- background: var(--fr-border);
6114
- border-radius: 4px;
6115
- height: 20px;
6116
- overflow: hidden;
6117
- }
6118
- .fr-progress__bar {
6119
- background: var(--fr-primary);
6120
- height: 100%;
6121
- transition: width 0.3s ease;
6122
- }
6123
- .fr-progress__text {
6124
- font-size: var(--fr-font-sm);
6125
- color: var(--fr-text-muted);
6126
- margin-top: 4px;
6127
- }
6128
-
6129
- .fr-progress__segments {
6130
- display: flex;
6131
- height: 100%;
6132
- width: 100%;
6133
- }
6134
- .fr-progress-segment {
6135
- height: 100%;
6136
- min-width: 2px;
6137
- border-right: 2px solid var(--fr-bg);
6138
- cursor: pointer;
6139
- }
6140
- .fr-progress-segment:last-child {
6141
- border-right: none;
6142
- }
6143
- .fr-progress-segment--filled {
6144
- background: var(--fr-primary);
6145
- }
6146
- .fr-progress-segment--filled:hover {
6147
- background: color-mix(in srgb, var(--fr-primary) 70%, white);
6148
- }
6149
- .fr-progress-segment--prefilled {
6150
- background: #8b5cf6;
6151
- }
6152
- .fr-progress-segment--prefilled:hover {
6153
- background: color-mix(in srgb, #8b5cf6 70%, white);
6154
- }
6155
- .fr-progress-segment--skipped {
6156
- background: var(--fr-warning);
6157
- }
6158
- .fr-progress-segment--skipped:hover {
6159
- background: color-mix(in srgb, var(--fr-warning) 70%, white);
6160
- }
6161
- .fr-progress-segment--empty {
6162
- background: var(--fr-border);
6163
- }
6164
-
6165
- /* Gantt chart - each call on its own row */
6166
- .fr-gantt {
6167
- margin-bottom: 8px;
6168
- }
6169
- .fr-gantt__row {
6170
- display: flex;
6171
- align-items: center;
6172
- height: 20px;
6173
- margin-bottom: 3px;
6174
- }
6175
- .fr-gantt__label {
6176
- width: 90px;
6177
- flex-shrink: 0;
6178
- font-size: 11px;
6179
- color: var(--fr-text-muted);
6180
- white-space: nowrap;
6181
- overflow: hidden;
6182
- text-overflow: ellipsis;
6183
- padding-right: 8px;
6184
- text-align: right;
6185
- }
6186
- .fr-gantt__track {
6187
- flex: 1;
6188
- background: var(--fr-bg-subtle);
6189
- border-radius: 3px;
6190
- height: 14px;
6191
- position: relative;
6192
- }
6193
- .fr-gantt__bar {
6194
- position: absolute;
6195
- top: 2px;
6196
- height: calc(100% - 4px);
6197
- min-width: 6px;
6198
- border-radius: 2px;
6199
- cursor: pointer;
6200
- }
6201
- .fr-gantt__bar:hover {
6202
- filter: brightness(1.15);
6203
- }
6204
- .fr-gantt__bar--llm {
6205
- background: var(--fr-primary);
6206
- }
6207
- .fr-gantt__bar--tool {
6208
- background: var(--fr-success);
6209
- }
6210
- .fr-gantt__legend {
6211
- display: flex;
6212
- gap: 16px;
6213
- font-size: var(--fr-font-sm);
6214
- color: var(--fr-text-muted);
6215
- margin-top: 12px;
6216
- padding-top: 8px;
6217
- border-top: 1px solid var(--fr-border);
6218
- }
6219
- .fr-gantt__legend-item {
6220
- display: flex;
6221
- align-items: center;
6222
- gap: 6px;
6223
- }
6224
- .fr-gantt__legend-dot {
6225
- width: 10px;
6226
- height: 10px;
6227
- border-radius: 2px;
6228
- }
6229
- .fr-gantt__legend-dot--llm { background: var(--fr-primary); }
6230
- .fr-gantt__legend-dot--tool { background: var(--fr-success); }
6231
-
6232
- /* Tooltip container */
6233
- .fr-tooltip {
6234
- position: fixed;
6235
- background: #1f2937;
6236
- color: #f9fafb;
6237
- padding: 8px 12px;
6238
- border-radius: 4px;
6239
- font-size: var(--fr-font-sm);
6240
- white-space: pre-line;
6241
- pointer-events: none;
6242
- z-index: 1000;
6243
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
6244
- opacity: 0;
6245
- visibility: hidden;
6246
- transition: opacity 0.05s ease-out, visibility 0.05s ease-out;
6247
- }
6248
- .fr-tooltip.visible {
6249
- opacity: 1;
6250
- visibility: visible;
6251
- transition: opacity 0.2s ease-in, visibility 0.2s ease-in;
6252
- }
6253
-
6254
- .fr-table {
6255
- width: 100%;
6256
- border-collapse: collapse;
6257
- font-size: var(--fr-font-sm);
6258
- }
6259
- .fr-table th {
6260
- padding: 8px 12px;
6261
- text-align: left;
6262
- font-weight: 600;
6263
- background: var(--fr-bg-subtle);
6264
- }
6265
- .fr-table th:not(:first-child) { text-align: center; }
6266
- .fr-table td {
6267
- padding: 8px 12px;
6268
- border-bottom: 1px solid var(--fr-border);
6269
- }
6270
- .fr-table td:not(:first-child) { text-align: center; }
6271
-
6272
- .fr-details {
6273
- border: none;
6274
- background: none;
6275
- }
6276
- .fr-details > summary {
6277
- cursor: pointer;
6278
- font-size: var(--fr-font-base);
6279
- font-weight: 500;
6280
- color: var(--fr-text);
6281
- padding: 8px 0;
6282
- list-style: none;
6283
- }
6284
- .fr-details > summary::-webkit-details-marker { display: none; }
6285
- .fr-details > summary::before {
6286
- content: '▶';
6287
- display: inline-block;
6288
- margin-right: 8px;
6289
- transition: transform 0.2s;
6290
- font-size: 11px;
6291
- }
6292
- .fr-details[open] > summary::before {
6293
- transform: rotate(90deg);
6294
- }
6295
- .fr-details__content {
6296
- background: var(--fr-bg-muted);
6297
- border-radius: 8px;
6298
- padding: 16px;
6299
- margin-top: 8px;
6300
- }
6301
-
6302
- .fr-turn {
6303
- margin-bottom: 8px;
6304
- background: var(--fr-bg-muted);
6305
- border-radius: 4px;
6306
- }
6307
- .fr-turn summary {
6308
- cursor: pointer;
6309
- padding: 12px;
6310
- font-size: var(--fr-font-sm);
6311
- list-style: none;
6312
- }
6313
- .fr-turn summary::-webkit-details-marker { display: none; }
6314
- .fr-turn summary::before {
6315
- content: '▶';
6316
- display: inline-block;
6317
- margin-right: 8px;
6318
- transition: transform 0.2s;
6319
- font-size: 11px;
6320
- }
6321
- .fr-turn[open] summary::before {
6322
- transform: rotate(90deg);
6323
- }
6324
- .fr-turn__content {
6325
- padding: 0 12px 12px;
6326
- }
6327
- .fr-turn__tools {
6328
- margin: 0;
6329
- padding-left: 20px;
6330
- list-style: none;
6331
- }
6332
- .fr-turn__tool {
6333
- margin: 4px 0;
6334
- font-size: var(--fr-font-sm);
6335
- color: var(--fr-text-muted);
6336
- }
6337
- .fr-turn__tool--error { color: var(--fr-error); }
6338
-
6339
- .fr-turn__query {
6340
- color: var(--fr-primary);
6341
- font-style: italic;
6342
- }
6343
-
6344
- .fr-turn__patches {
6345
- margin: 4px 0 8px 20px;
6346
- padding: 8px 12px;
6347
- background: var(--fr-bg-subtle);
6348
- border-radius: 4px;
6349
- font-size: var(--fr-font-sm);
6350
- }
6351
- .fr-turn__patch {
6352
- margin: 4px 0;
6353
- padding: 4px 0;
6354
- border-bottom: 1px solid var(--fr-border);
6355
- }
6356
- .fr-turn__patch:last-child {
6357
- border-bottom: none;
6358
- margin-bottom: 0;
6359
- padding-bottom: 0;
6360
- }
6361
- .fr-turn__patch-field {
6362
- font-weight: 600;
6363
- color: var(--fr-text);
6364
- }
6365
- .fr-turn__patch-op {
6366
- font-size: 11px;
6367
- padding: 1px 4px;
6368
- border-radius: 2px;
6369
- background: var(--fr-bg-muted);
6370
- color: var(--fr-text-muted);
6371
- margin-left: 6px;
6372
- }
6373
- .fr-turn__patch-value {
6374
- display: block;
6375
- margin-top: 2px;
6376
- color: var(--fr-text-muted);
6377
- font-family: ui-monospace, 'SF Mono', Menlo, monospace;
6378
- word-break: break-word;
6379
- white-space: pre-wrap;
6380
- max-height: 200px;
6381
- overflow: auto;
6382
- }
6383
- .fr-turn__patch-value--skip {
6384
- color: var(--fr-warning);
6385
- font-style: italic;
6386
- }
6387
- .fr-turn__patch-value--clear {
6388
- color: var(--fr-info);
6389
- font-style: italic;
6390
- }
6391
-
6392
- .fr-raw {
6393
- position: relative;
6394
- }
6395
- .fr-copy-btn {
6396
- position: absolute;
6397
- top: 8px;
6398
- right: 8px;
6399
- padding: 4px 8px;
6400
- font-size: var(--fr-font-sm);
6401
- background: var(--fr-bg-subtle);
6402
- border: 1px solid var(--fr-border);
6403
- border-radius: 4px;
6404
- cursor: pointer;
6405
- color: var(--fr-text-muted);
6406
- transition: all 0.15s;
6407
- }
6408
- .fr-copy-btn:hover {
6409
- background: var(--fr-border);
6410
- color: var(--fr-text);
6411
- }
6412
- .fr-copy-btn:active {
6413
- transform: scale(0.95);
6414
- }
6415
-
6416
- /* Scoped pre styles to override parent .tab-content pre */
6417
- .fr-dashboard pre {
6418
- background: var(--fr-bg-muted);
6419
- color: var(--fr-text);
6420
- padding: 1rem;
6421
- border-radius: 6px;
6422
- border: 1px solid var(--fr-border);
6423
- overflow-x: auto;
6424
- font-family: ui-monospace, 'SF Mono', Menlo, monospace;
6425
- font-size: 0.85rem;
6426
- line-height: 1.5;
6427
- margin: 0;
6428
- }
6429
-
6430
- /* Override syntax highlighting colors for dark mode compatibility */
6431
- .fr-dashboard .syn-key { color: var(--fr-primary); }
6432
- .fr-dashboard .syn-string { color: var(--fr-success); }
6433
- .fr-dashboard .syn-number { color: var(--fr-primary); }
6434
- .fr-dashboard .syn-bool { color: var(--fr-warning); }
6435
- .fr-dashboard .syn-null { color: var(--fr-error); }
6436
-
6437
- @media (max-width: 600px) {
6438
- .fr-dashboard { padding: 12px; }
6439
- .fr-cards { grid-template-columns: repeat(2, 1fr); gap: 12px; }
6440
- .fr-card { padding: 12px; }
6441
- .fr-card__value { font-size: 18px; }
6442
- .fr-table { font-size: var(--fr-font-sm); }
6443
- .fr-table th, .fr-table td { padding: 6px 8px; }
6444
- }
6445
- </style>
6446
- `;
6447
- /**
6448
- * Render fill record content (dashboard-style visualization).
6449
- * Uses CSS custom properties for theming with automatic dark mode support.
6450
- * Mobile responsive with grid-based layout.
6451
- *
6452
- * @public Exported for testing and reuse.
6453
- */
6454
- function renderFillRecordContent(record) {
6455
- const { status, statusDetail, startedAt, durationMs, llm, formProgress, toolSummary, timeline } = record;
6456
- const startDate = new Date(startedAt);
6457
- const formattedDate = startDate.toLocaleDateString("en-US", {
6458
- month: "short",
6459
- day: "numeric",
6460
- year: "numeric"
6461
- });
6462
- const formattedTime = startDate.toLocaleTimeString("en-US", {
6463
- hour: "numeric",
6464
- minute: "2-digit",
6465
- hour12: true
6466
- });
6467
- const headerInfo = `
6468
- <div class="fr-header">
6469
- <div class="fr-header__model">${escapeHtml(llm.model)}</div>
6470
- <div class="fr-header__time">${formattedDate} at ${formattedTime}</div>
6471
- </div>
6472
- `;
6473
- let statusBanner = "";
6474
- if (status !== "completed") {
6475
- const bannerClass = status === "failed" ? "fr-banner--error" : "fr-banner--warning";
6476
- const icon = status === "failed" ? "✕" : "⚠";
6477
- const title = status === "failed" ? "FAILED" : status === "cancelled" ? "CANCELLED" : "PARTIAL";
6478
- const msg = statusDetail ?? (status === "partial" ? "Did not complete all fields" : "");
6479
- statusBanner = `<div class="fr-banner ${bannerClass}"><strong>${icon} ${title}${msg ? ":" : ""}</strong>${msg ? ` ${escapeHtml(msg)}` : ""}</div>`;
6480
- }
6481
- const totalTokens = llm.inputTokens + llm.outputTokens;
6482
- const summaryCards = `
6483
- <div class="fr-cards">
6484
- <div class="fr-card">
6485
- <div class="fr-card__label">Status</div>
6486
- <div><span class="${`fr-badge fr-badge--${status}`}">${{
6487
- completed: "✓",
6488
- partial: "⚠",
6489
- cancelled: "⊘",
6490
- failed: "✕"
6491
- }[status] ?? "?"} ${status.charAt(0).toUpperCase() + status.slice(1)}</span></div>
6492
- </div>
6493
- <div class="fr-card">
6494
- <div class="fr-card__label">Duration</div>
6495
- <div class="fr-card__value">${formatDuration(durationMs)}</div>
6496
- </div>
6497
- <div class="fr-card">
6498
- <div class="fr-card__label">Turns</div>
6499
- <div class="fr-card__value">${timeline.length}</div>
6500
- </div>
6501
- <div class="fr-card">
6502
- <div class="fr-card__label">Tokens</div>
6503
- <div class="fr-card__value">${formatTokens(totalTokens)}</div>
6504
- <div class="fr-card__sub">${formatTokens(llm.inputTokens)} in / ${formatTokens(llm.outputTokens)} out</div>
6505
- </div>
6506
- </div>
6507
- `;
6508
- const fieldsMap = /* @__PURE__ */ new Map();
6509
- for (const turn of timeline) for (const tc of turn.toolCalls) if (tc.tool === "fill_form" && tc.input.patches) {
6510
- const patches = tc.input.patches;
6511
- for (const patch of patches) if (patch.fieldId && patch.op) fieldsMap.set(patch.fieldId, {
6512
- fieldId: patch.fieldId,
6513
- op: patch.op,
6514
- turnNumber: turn.turnNumber
6515
- });
6516
- }
6517
- const fieldsFilled = Array.from(fieldsMap.values());
6518
- const totalFields = formProgress.totalFields;
6519
- const filledFields = formProgress.filledFields;
6520
- const skippedFields = formProgress.skippedFields;
6521
- const abortedFields = formProgress.abortedFields ?? 0;
6522
- const progressPercent = totalFields > 0 ? Math.round(filledFields / totalFields * 100) : 0;
6523
- const segmentWidth = totalFields > 0 ? 100 / totalFields : 0;
6524
- const aiFilledFields = fieldsFilled.filter((f) => f.op !== "skip_field" && f.op !== "abort_field");
6525
- const aiFilledSegmentsHtml = aiFilledFields.map((f) => {
6526
- const opLabel = f.op.replace(/_/g, " ");
6527
- 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>`;
6528
- }).join("");
6529
- const prefilledCount = Math.max(0, filledFields - aiFilledFields.length);
6530
- 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>` : "";
6531
- const skippedSegmentsHtml = fieldsFilled.filter((f) => f.op === "skip_field" || f.op === "abort_field").map((f) => {
6532
- const opLabel = f.op === "skip_field" ? "skipped" : "aborted";
6533
- 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>`;
6534
- }).join("");
6535
- const unfilledCount = totalFields - filledFields - skippedFields - abortedFields;
6536
- const unfilledSegmentsHtml = unfilledCount > 0 ? `<div class="fr-progress-segment fr-progress-segment--empty" style="width: ${segmentWidth * unfilledCount}%"></div>` : "";
6537
- const progressDetails = [];
6538
- if (prefilledCount > 0) progressDetails.push(`${prefilledCount} pre-filled`);
6539
- if (skippedFields > 0) progressDetails.push(`${skippedFields} skipped`);
6540
- const progressBar = `
6541
- <div class="fr-section">
6542
- <div class="fr-section__title">Progress</div>
6543
- <div class="fr-progress">
6544
- <div class="fr-progress__segments">
6545
- ${prefilledSegmentsHtml}${aiFilledSegmentsHtml}${skippedSegmentsHtml}${unfilledSegmentsHtml}
6546
- </div>
6547
- </div>
6548
- <div class="fr-progress__text">
6549
- ${filledFields}/${totalFields} fields filled (${progressPercent}%)${progressDetails.length > 0 ? ` • ${progressDetails.join(" • ")}` : ""}
6550
- </div>
6551
- </div>
6552
- `;
6553
- const totalMs = durationMs;
6554
- const llmCallCount = llm.totalCalls;
6555
- const toolCallCount = toolSummary.totalCalls;
6556
- const timelineEvents = [];
6557
- for (const turn of timeline) {
6558
- const toolTimeInTurn = turn.toolCalls.reduce((sum, tc) => sum + tc.durationMs, 0);
6559
- const llmTimeInTurn = Math.max(0, turn.durationMs - toolTimeInTurn);
6560
- if (llmTimeInTurn > 0) timelineEvents.push({
6561
- type: "llm",
6562
- startMs: turn.startMs,
6563
- durationMs: llmTimeInTurn,
6564
- turnNumber: turn.turnNumber,
6565
- label: `Turn ${turn.turnNumber}`,
6566
- tokens: {
6567
- input: turn.tokens.input,
6568
- output: turn.tokens.output,
6569
- total: turn.tokens.input + turn.tokens.output
6570
- }
6571
- });
6572
- for (const tc of turn.toolCalls) timelineEvents.push({
6573
- type: "tool",
6574
- startMs: tc.startMs,
6575
- durationMs: tc.durationMs,
6576
- turnNumber: turn.turnNumber,
6577
- label: tc.tool
6578
- });
6579
- }
6580
- const ganttRowsHtml = timelineEvents.map((e) => {
6581
- const leftPct = totalMs > 0 ? e.startMs / totalMs * 100 : 0;
6582
- const widthPct = totalMs > 0 ? e.durationMs / totalMs * 100 : 0;
6583
- const barClass = e.type === "llm" ? "fr-gantt__bar--llm" : "fr-gantt__bar--tool";
6584
- const startTime = `Start: ${formatDuration(e.startMs)}`;
6585
- const tooltip = e.type === "llm" ? `${e.label}&#10;${startTime}&#10;Duration: ${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;${startTime}&#10;Duration: ${formatDuration(e.durationMs)}&#10;Turn ${e.turnNumber}`;
6586
- return `
6587
- <div class="fr-gantt__row">
6588
- <div class="fr-gantt__label">${escapeHtml(e.label)}</div>
6589
- <div class="fr-gantt__track">
6590
- <div class="fr-gantt__bar ${barClass}" style="left: ${leftPct}%; width: ${widthPct}%" data-tooltip="${tooltip}" onmouseenter="frShowTip(this)" onmouseleave="frHideTip()"></div>
6591
- </div>
6592
- </div>`;
6593
- }).join("");
6594
- const llmTotalMs = timelineEvents.filter((e) => e.type === "llm").reduce((sum, e) => sum + e.durationMs, 0);
6595
- const toolTotalMs = timelineEvents.filter((e) => e.type === "tool").reduce((sum, e) => sum + e.durationMs, 0);
6596
- const timingSection = `
6597
- <details class="fr-details fr-section" open>
6598
- <summary>Timeline (${formatDuration(totalMs)} total)</summary>
6599
- <div class="fr-details__content">
6600
- <div class="fr-gantt">
6601
- ${ganttRowsHtml}
6602
- <div class="fr-gantt__legend">
6603
- <div class="fr-gantt__legend-item">
6604
- <div class="fr-gantt__legend-dot fr-gantt__legend-dot--llm"></div>
6605
- <span>LLM (${llmCallCount} call${llmCallCount !== 1 ? "s" : ""}, ${formatDuration(llmTotalMs)})</span>
6606
- </div>
6607
- <div class="fr-gantt__legend-item">
6608
- <div class="fr-gantt__legend-dot fr-gantt__legend-dot--tool"></div>
6609
- <span>Tools (${toolCallCount} call${toolCallCount !== 1 ? "s" : ""}, ${formatDuration(toolTotalMs)})</span>
6610
- </div>
6611
- </div>
6612
- </div>
6613
- </div>
6614
- </details>
6615
- `;
6616
- let toolSection = "";
6617
- if (toolSummary.byTool.length > 0) toolSection = `
6618
- <details class="fr-details fr-section" open>
6619
- <summary>Tool Summary</summary>
6620
- <div style="overflow-x: auto; margin-top: 8px;">
6621
- <table class="fr-table">
6622
- <thead><tr><th>Tool</th><th>Calls</th><th>Success</th><th>Avg</th><th>p95</th></tr></thead>
6623
- <tbody>${toolSummary.byTool.map((t) => `
6624
- <tr>
6625
- <td>${escapeHtml(t.toolName)}</td>
6626
- <td>${t.callCount}</td>
6627
- <td>${t.successCount === t.callCount ? "100%" : `${Math.round(t.successCount / t.callCount * 100)}%`}</td>
6628
- <td>${formatDuration(t.timing.avgMs)}</td>
6629
- <td>${formatDuration(t.timing.p95Ms)}</td>
6630
- </tr>
6631
- `).join("")}</tbody>
6632
- </table>
6633
- </div>
6634
- </details>
6635
- `;
6636
- let timelineSection = "";
6637
- if (timeline.length > 0) {
6638
- const timelineItems = timeline.map((turn) => {
6639
- const turnTokens = turn.tokens.input + turn.tokens.output;
6640
- const toolCallsList = turn.toolCalls.map((tc) => renderToolCall(tc)).join("");
6641
- const patchInfo = turn.patchesApplied > 0 ? ` • ${turn.patchesApplied} patches` : "";
6642
- const rejectedInfo = turn.patchesRejected > 0 ? ` <span style="color: var(--fr-error)">(${turn.patchesRejected} rejected)</span>` : "";
6643
- return `
6644
- <details class="fr-turn">
6645
- <summary><strong>Turn ${turn.turnNumber}</strong> • Order ${turn.order} • ${formatDuration(turn.durationMs)} • ${formatTokens(turnTokens)} tokens${patchInfo}${rejectedInfo}</summary>
6646
- <div class="fr-turn__content">
6647
- ${turn.toolCalls.length > 0 ? `<ul class="fr-turn__tools">${toolCallsList}</ul>` : "<span class=\"fr-turn__tool\">No tool calls</span>"}
6648
- </div>
6649
- </details>
6650
- `;
6651
- }).join("");
6652
- timelineSection = `
6653
- <details class="fr-details fr-section">
6654
- <summary>Turn Details (${timeline.length} turns)</summary>
6655
- <div style="margin-top: 8px;">${timelineItems}</div>
6656
- </details>
6657
- `;
6658
- }
6659
- const rawSection = `
6660
- <details class="fr-details fr-section">
6661
- <summary>Raw YAML</summary>
6662
- <div class="fr-raw" style="margin-top: 8px;">
6663
- <button class="fr-copy-btn" onclick="frCopyYaml(this)">Copy</button>
6664
- ${renderYamlContent(YAML.stringify(record, { lineWidth: 0 }))}
6665
- </div>
6666
- </details>
6667
- `;
6668
- return `
6669
- ${FILL_RECORD_STYLES}
6670
- <div id="fr-tooltip" class="fr-tooltip"></div>
6671
- <div class="fr-dashboard">
6672
- ${headerInfo}
6673
- ${statusBanner}
6674
- ${summaryCards}
6675
- ${progressBar}
6676
- ${timingSection}
6677
- ${toolSection}
6678
- ${timelineSection}
6679
- ${rawSection}
6680
- </div>
6681
- `;
6682
- }
6683
5494
 
6684
5495
  //#endregion
6685
5496
  //#region src/cli/commands/render.ts
@@ -6874,9 +5685,9 @@ function registerResearchCommand(program) {
6874
5685
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
6875
5686
  console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
6876
5687
  if (options.transcript && result.transcript) {
6877
- const { serializeSession } = await import("./session-BPuQ-ok0.mjs");
5688
+ const { serializeSession } = await import("./session-VeSkVrck.mjs");
6878
5689
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
6879
- const { writeFile } = await import("./shared-BTR35aMz.mjs");
5690
+ const { writeFile } = await import("./shared-fb0nkzQi.mjs");
6880
5691
  await writeFile(transcriptPath, serializeSession(result.transcript));
6881
5692
  logInfo(ctx, `Transcript: ${transcriptPath}`);
6882
5693
  }
@@ -7281,4 +6092,4 @@ async function runCli() {
7281
6092
 
7282
6093
  //#endregion
7283
6094
  export { runCli as t };
7284
- //# sourceMappingURL=cli-ChdIy1a7.mjs.map
6095
+ //# sourceMappingURL=cli-ZcOC47KK.mjs.map