markform 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -55
- package/dist/ai-sdk.d.mts +1 -1
- package/dist/ai-sdk.mjs +2 -2
- package/dist/{apply-BUU2QcJ2.mjs → apply-BTFCHpYV.mjs} +402 -341
- package/dist/bin.mjs +1 -1
- package/dist/{cli-BZh25bvy.mjs → cli-BZa4fYR9.mjs} +518 -65
- package/dist/cli.mjs +1 -1
- package/dist/{coreTypes-DJtu8OOp.mjs → coreTypes-B1oI7qvV.mjs} +36 -3
- package/dist/{coreTypes-BSPJ9H27.d.mts → coreTypes-JCPm418M.d.mts} +215 -9
- package/dist/index.d.mts +180 -17
- package/dist/index.mjs +5 -5
- package/dist/{session-DSTNiHza.mjs → session-CzCh6JeY.mjs} +1 -1
- package/dist/{session-CmHdAPyg.mjs → session-Dxqwt0RC.mjs} +3 -3
- package/dist/{shared-C9yW5FLZ.mjs → shared-CNqwaxUt.mjs} +1 -1
- package/dist/{shared-DQ6y3Ggc.mjs → shared-D3dNi-Gn.mjs} +1 -1
- package/dist/{src-kUggXhN1.mjs → src-CYnyLwBe.mjs} +297 -128
- package/docs/markform-spec.md +104 -53
- package/examples/movie-research/movie-deep-research-mock-filled.form.md +601 -0
- package/examples/movie-research/{movie-research-deep.form.md → movie-deep-research.form.md} +2 -2
- package/examples/rejection-test/rejection-test.session.yaml +446 -0
- package/examples/simple/simple-with-skips.session.yaml +1966 -18
- package/examples/simple/simple.session.yaml +1979 -18
- package/examples/startup-deep-research/startup-deep-research.form.md +21 -36
- package/package.json +1 -1
- package/examples/earnings-analysis/earnings-analysis.form.md +0 -159
- package/examples/earnings-analysis/earnings-analysis.raw.md +0 -801
- package/examples/earnings-analysis/earnings-analysis.valid.ts +0 -198
- package/examples/movie-research/movie-research-basic.form.md +0 -169
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import { L as PatchSchema } from "./coreTypes-
|
|
3
|
-
import { A as
|
|
4
|
-
import {
|
|
5
|
-
import { n as serializeSession } from "./session-
|
|
6
|
-
import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, 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-
|
|
2
|
+
import { L as PatchSchema } from "./coreTypes-B1oI7qvV.mjs";
|
|
3
|
+
import { A as deriveReportPath, C as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, D as REPORT_EXTENSION, E as MAX_FORMS_IN_MENU, F as WEB_SEARCH_CONFIG, I as formatSuggestedLlms, M as detectFileType, N as parseRolesFlag, O as USER_ROLE, P as SUGGESTED_LLMS, R as hasWebSearchSupport, S as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, _ as DEFAULT_MAX_ISSUES_PER_TURN, b as DEFAULT_PORT, d as serialize, f as serializeRawMarkdown, g as DEFAULT_FORMS_DIR, h as ALL_EXTENSIONS, i as inspect, j as deriveSchemaPath, k as deriveExportPath, m as AGENT_ROLE, n as getAllFields, p as serializeReportMarkdown, t as applyPatches, v as DEFAULT_MAX_PATCHES_PER_TURN, y as DEFAULT_MAX_TURNS, z as parseModelIdForDisplay } from "./apply-BTFCHpYV.mjs";
|
|
4
|
+
import { D as parseForm, E as formToJsonSchema, a as resolveHarnessConfig, c as getProviderNames, d as createLiveAgent, h as createHarness, i as runResearch, l as resolveModel, n as isResearchForm, o as fillForm, p as createMockAgent, s as getProviderInfo, t as VERSION, u as buildMockWireFormat } from "./src-CYnyLwBe.mjs";
|
|
5
|
+
import { n as serializeSession } from "./session-Dxqwt0RC.mjs";
|
|
6
|
+
import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, 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-D3dNi-Gn.mjs";
|
|
7
|
+
import Markdoc from "@markdoc/markdoc";
|
|
7
8
|
import YAML from "yaml";
|
|
8
9
|
import { Command } from "commander";
|
|
9
10
|
import pc from "picocolors";
|
|
@@ -942,7 +943,16 @@ function formatFieldResponse(response, useColors) {
|
|
|
942
943
|
}
|
|
943
944
|
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
944
945
|
case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
945
|
-
|
|
946
|
+
case "date": return value.value ? green(value.value) : dim("(empty)");
|
|
947
|
+
case "year": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
948
|
+
case "table": {
|
|
949
|
+
const rowCount = value.rows?.length ?? 0;
|
|
950
|
+
return rowCount > 0 ? green(`(${rowCount} rows)`) : dim("(empty)");
|
|
951
|
+
}
|
|
952
|
+
default: {
|
|
953
|
+
const _exhaustive = value;
|
|
954
|
+
throw new Error(`Unhandled field value kind: ${_exhaustive.kind}`);
|
|
955
|
+
}
|
|
946
956
|
}
|
|
947
957
|
}
|
|
948
958
|
/**
|
|
@@ -1014,15 +1024,9 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
1014
1024
|
type: "fill"
|
|
1015
1025
|
},
|
|
1016
1026
|
{
|
|
1017
|
-
id: "movie-research
|
|
1018
|
-
filename: "movie-research
|
|
1019
|
-
path: "movie-research/movie-research
|
|
1020
|
-
type: "research"
|
|
1021
|
-
},
|
|
1022
|
-
{
|
|
1023
|
-
id: "movie-research-deep",
|
|
1024
|
-
filename: "movie-research-deep.form.md",
|
|
1025
|
-
path: "movie-research/movie-research-deep.form.md",
|
|
1027
|
+
id: "movie-deep-research",
|
|
1028
|
+
filename: "movie-deep-research.form.md",
|
|
1029
|
+
path: "movie-research/movie-deep-research.form.md",
|
|
1026
1030
|
type: "research"
|
|
1027
1031
|
},
|
|
1028
1032
|
{
|
|
@@ -1030,12 +1034,6 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
1030
1034
|
filename: "startup-deep-research.form.md",
|
|
1031
1035
|
path: "startup-deep-research/startup-deep-research.form.md",
|
|
1032
1036
|
type: "research"
|
|
1033
|
-
},
|
|
1034
|
-
{
|
|
1035
|
-
id: "earnings-analysis",
|
|
1036
|
-
filename: "earnings-analysis.form.md",
|
|
1037
|
-
path: "earnings-analysis/earnings-analysis.form.md",
|
|
1038
|
-
type: "research"
|
|
1039
1037
|
}
|
|
1040
1038
|
];
|
|
1041
1039
|
/** Default example ID for menus (movie-research-demo, index 0) */
|
|
@@ -1059,7 +1057,7 @@ function getExamplesDir() {
|
|
|
1059
1057
|
}
|
|
1060
1058
|
/**
|
|
1061
1059
|
* Load the content of an example form.
|
|
1062
|
-
* @param exampleId - The example ID (e.g., 'simple', 'movie-research
|
|
1060
|
+
* @param exampleId - The example ID (e.g., 'simple', 'movie-deep-research')
|
|
1063
1061
|
* @returns The form content as a string
|
|
1064
1062
|
* @throws Error if the example is not found
|
|
1065
1063
|
*/
|
|
@@ -1070,7 +1068,7 @@ function loadExampleContent(exampleId) {
|
|
|
1070
1068
|
try {
|
|
1071
1069
|
return readFileSync(filePath, "utf-8");
|
|
1072
1070
|
} catch (error) {
|
|
1073
|
-
throw new Error(`Failed to load example '${exampleId}' from ${filePath}: ${error}`);
|
|
1071
|
+
throw new Error(`Failed to load example '${exampleId}' from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1074
1072
|
}
|
|
1075
1073
|
}
|
|
1076
1074
|
/**
|
|
@@ -1081,7 +1079,7 @@ function getExampleById(id) {
|
|
|
1081
1079
|
}
|
|
1082
1080
|
/**
|
|
1083
1081
|
* Get the absolute path to an example's source file.
|
|
1084
|
-
* @param exampleId - The example ID (e.g., 'simple', 'movie-research
|
|
1082
|
+
* @param exampleId - The example ID (e.g., 'simple', 'movie-deep-research')
|
|
1085
1083
|
* @returns The absolute path to the example form file
|
|
1086
1084
|
* @throws Error if the example is not found
|
|
1087
1085
|
*/
|
|
@@ -1091,27 +1089,27 @@ function getExamplePath(exampleId) {
|
|
|
1091
1089
|
return join(getExamplesDir(), example.path);
|
|
1092
1090
|
}
|
|
1093
1091
|
/**
|
|
1094
|
-
* Extract YAML frontmatter from
|
|
1092
|
+
* Extract YAML frontmatter from markdown content using Markdoc's native support.
|
|
1095
1093
|
* @param content - The markdown file content
|
|
1096
1094
|
* @returns The parsed frontmatter object or null if no frontmatter found
|
|
1097
1095
|
*/
|
|
1098
1096
|
function extractFrontmatter(content) {
|
|
1099
|
-
const
|
|
1100
|
-
if (!
|
|
1097
|
+
const rawFrontmatter = Markdoc.parse(content).attributes.frontmatter;
|
|
1098
|
+
if (!rawFrontmatter) return null;
|
|
1101
1099
|
try {
|
|
1102
|
-
return YAML.parse(
|
|
1100
|
+
return YAML.parse(rawFrontmatter);
|
|
1103
1101
|
} catch {
|
|
1104
1102
|
return null;
|
|
1105
1103
|
}
|
|
1106
1104
|
}
|
|
1107
1105
|
/**
|
|
1108
1106
|
* Load metadata (title, description) from an example's YAML frontmatter.
|
|
1109
|
-
* @param exampleId - The example ID (e.g., 'simple', 'movie-research
|
|
1107
|
+
* @param exampleId - The example ID (e.g., 'simple', 'movie-deep-research')
|
|
1110
1108
|
* @returns Object with title and description from frontmatter
|
|
1111
1109
|
*/
|
|
1112
1110
|
function loadExampleMetadata(exampleId) {
|
|
1113
1111
|
const frontmatter = extractFrontmatter(loadExampleContent(exampleId));
|
|
1114
|
-
if (!frontmatter
|
|
1112
|
+
if (!frontmatter?.markform) return {};
|
|
1115
1113
|
const markform = frontmatter.markform;
|
|
1116
1114
|
return {
|
|
1117
1115
|
title: typeof markform.title === "string" ? markform.title : void 0,
|
|
@@ -1147,7 +1145,7 @@ function issueReasonToStatus(reason) {
|
|
|
1147
1145
|
case "validation_error": return "invalid";
|
|
1148
1146
|
case "checkbox_incomplete": return "incomplete";
|
|
1149
1147
|
case "min_items_not_met": return "too-few";
|
|
1150
|
-
case "
|
|
1148
|
+
case "optional_unanswered": return "unanswered";
|
|
1151
1149
|
default: return "issue";
|
|
1152
1150
|
}
|
|
1153
1151
|
}
|
|
@@ -1170,7 +1168,7 @@ function formatTurnIssues(issues, maxShow = 5) {
|
|
|
1170
1168
|
/**
|
|
1171
1169
|
* Format form info for menu label display.
|
|
1172
1170
|
* Format: "filename - Title [runMode]"
|
|
1173
|
-
* Example: "movie-research
|
|
1171
|
+
* Example: "movie-deep-research.form.md - Movie Deep Research [research]"
|
|
1174
1172
|
*/
|
|
1175
1173
|
function formatFormLabel(info) {
|
|
1176
1174
|
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
@@ -1187,7 +1185,7 @@ function formatFormHint(info) {
|
|
|
1187
1185
|
/**
|
|
1188
1186
|
* Format form info for log line (e.g., after copying).
|
|
1189
1187
|
* Format: "filename - Title" (dimmed title)
|
|
1190
|
-
* Example: "✓ movie-research
|
|
1188
|
+
* Example: "✓ movie-deep-research.form.md - Movie Deep Research"
|
|
1191
1189
|
*/
|
|
1192
1190
|
function formatFormLogLine(info, prefix) {
|
|
1193
1191
|
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
@@ -1955,7 +1953,10 @@ function formatPatchValue(patch) {
|
|
|
1955
1953
|
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
1956
1954
|
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1957
1955
|
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1958
|
-
case "set_table":
|
|
1956
|
+
case "set_table": {
|
|
1957
|
+
const rowCount = patch.rows?.length ?? 0;
|
|
1958
|
+
return rowCount > 0 ? truncate(`[${rowCount} rows]`) : "(empty)";
|
|
1959
|
+
}
|
|
1959
1960
|
case "add_note": return truncate(`note: ${patch.text}`);
|
|
1960
1961
|
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
1961
1962
|
}
|
|
@@ -2258,6 +2259,7 @@ async function runAgentFillWorkflow(form, modelId, formsDir, filePath, isResearc
|
|
|
2258
2259
|
targetRoles: [AGENT_ROLE],
|
|
2259
2260
|
fillMode: overwrite ? "overwrite" : "continue",
|
|
2260
2261
|
enableWebSearch: isResearch,
|
|
2262
|
+
captureWireFormat: false,
|
|
2261
2263
|
callbacks
|
|
2262
2264
|
});
|
|
2263
2265
|
if (result.status.ok) p.log.success(pc.green(`Form completed in ${result.turns} turn(s)`));
|
|
@@ -2851,6 +2853,7 @@ function registerFillCommand(program) {
|
|
|
2851
2853
|
let mockPath;
|
|
2852
2854
|
let agentProvider;
|
|
2853
2855
|
let agentModelName;
|
|
2856
|
+
let targetRole = AGENT_ROLE;
|
|
2854
2857
|
let currentSpinner = null;
|
|
2855
2858
|
if (options.mock) {
|
|
2856
2859
|
mockPath = resolve(options.mockSource);
|
|
@@ -2878,12 +2881,12 @@ function registerFillCommand(program) {
|
|
|
2878
2881
|
error: (msg) => currentSpinner?.error(msg),
|
|
2879
2882
|
getElapsedMs: () => currentSpinner?.getElapsedMs() ?? 0
|
|
2880
2883
|
}, ctx);
|
|
2881
|
-
|
|
2884
|
+
targetRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0] ?? AGENT_ROLE;
|
|
2882
2885
|
const liveAgent = createLiveAgent({
|
|
2883
2886
|
model,
|
|
2884
2887
|
provider,
|
|
2885
2888
|
systemPromptAddition: systemPrompt,
|
|
2886
|
-
targetRole
|
|
2889
|
+
targetRole,
|
|
2887
2890
|
enableWebSearch: true,
|
|
2888
2891
|
callbacks
|
|
2889
2892
|
});
|
|
@@ -2899,6 +2902,7 @@ function registerFillCommand(program) {
|
|
|
2899
2902
|
logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
|
|
2900
2903
|
logVerbose(ctx, `Fill mode: ${fillMode}`);
|
|
2901
2904
|
let stepResult = harness.step();
|
|
2905
|
+
let previousRejections;
|
|
2902
2906
|
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
2903
2907
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
2904
2908
|
let spinner = null;
|
|
@@ -2913,7 +2917,7 @@ function registerFillCommand(program) {
|
|
|
2913
2917
|
}
|
|
2914
2918
|
let response;
|
|
2915
2919
|
try {
|
|
2916
|
-
response = await agent.
|
|
2920
|
+
response = await agent.fillFormTool(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
2917
2921
|
spinner?.stop();
|
|
2918
2922
|
currentSpinner = null;
|
|
2919
2923
|
} catch (error) {
|
|
@@ -2933,7 +2937,7 @@ function registerFillCommand(program) {
|
|
|
2933
2937
|
}
|
|
2934
2938
|
if (stats) {
|
|
2935
2939
|
logVerbose(ctx, ` Stats: tokens ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0}`);
|
|
2936
|
-
if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
2940
|
+
if (stats.toolCalls && stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
2937
2941
|
if (stats.prompts) {
|
|
2938
2942
|
logVerbose(ctx, ``);
|
|
2939
2943
|
logVerbose(ctx, pc.dim(` ─── System Prompt ───`));
|
|
@@ -2945,12 +2949,28 @@ function registerFillCommand(program) {
|
|
|
2945
2949
|
}
|
|
2946
2950
|
}
|
|
2947
2951
|
let llmStats;
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2952
|
+
let context;
|
|
2953
|
+
let wire;
|
|
2954
|
+
if (stats) {
|
|
2955
|
+
llmStats = {
|
|
2956
|
+
inputTokens: stats.inputTokens,
|
|
2957
|
+
outputTokens: stats.outputTokens,
|
|
2958
|
+
toolCalls: stats.toolCalls && stats.toolCalls.length > 0 ? stats.toolCalls : void 0
|
|
2959
|
+
};
|
|
2960
|
+
if (stats.prompts) context = {
|
|
2961
|
+
systemPrompt: stats.prompts.system,
|
|
2962
|
+
contextPrompt: stats.prompts.context
|
|
2963
|
+
};
|
|
2964
|
+
wire = stats.wire;
|
|
2965
|
+
} else if (options.mock) {
|
|
2966
|
+
wire = buildMockWireFormat(harness.getForm(), stepResult.issues, patches, harnessConfig.maxPatchesPerTurn, targetRole, previousRejections);
|
|
2967
|
+
context = {
|
|
2968
|
+
systemPrompt: wire.request.system,
|
|
2969
|
+
contextPrompt: wire.request.prompt
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
stepResult = harness.apply(patches, stepResult.issues, llmStats, context, wire);
|
|
2973
|
+
previousRejections = stepResult.rejectedPatches;
|
|
2954
2974
|
if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
2955
2975
|
else if (!harness.hasReachedMaxTurns()) {
|
|
2956
2976
|
stepResult = harness.step();
|
|
@@ -3091,7 +3111,16 @@ function formatFieldValue(value, useColors) {
|
|
|
3091
3111
|
}
|
|
3092
3112
|
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
3093
3113
|
case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
|
|
3094
|
-
|
|
3114
|
+
case "date": return value.value ? green(value.value) : dim("(empty)");
|
|
3115
|
+
case "year": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
3116
|
+
case "table": {
|
|
3117
|
+
const rowCount = value.rows?.length ?? 0;
|
|
3118
|
+
return rowCount > 0 ? green(`(${rowCount} rows)`) : dim("(empty)");
|
|
3119
|
+
}
|
|
3120
|
+
default: {
|
|
3121
|
+
const _exhaustive = value;
|
|
3122
|
+
throw new Error(`Unhandled field value kind: ${_exhaustive.kind}`);
|
|
3123
|
+
}
|
|
3095
3124
|
}
|
|
3096
3125
|
}
|
|
3097
3126
|
/**
|
|
@@ -3504,6 +3533,57 @@ function openBrowser(url) {
|
|
|
3504
3533
|
});
|
|
3505
3534
|
}
|
|
3506
3535
|
/**
|
|
3536
|
+
* Get the base path by stripping any known markform extension.
|
|
3537
|
+
*/
|
|
3538
|
+
function getBasePath(filePath) {
|
|
3539
|
+
for (const ext of Object.values(ALL_EXTENSIONS)) if (filePath.endsWith(ext)) return filePath.slice(0, -ext.length);
|
|
3540
|
+
if (filePath.endsWith(".md")) return filePath.slice(0, -3);
|
|
3541
|
+
return filePath;
|
|
3542
|
+
}
|
|
3543
|
+
/**
|
|
3544
|
+
* Find related files for a form file.
|
|
3545
|
+
* Checks for .report.md, .yml, and .schema.json files with the same base name.
|
|
3546
|
+
*/
|
|
3547
|
+
function findRelatedFiles(formPath) {
|
|
3548
|
+
const base = getBasePath(formPath);
|
|
3549
|
+
const reportPath = base + ALL_EXTENSIONS.report;
|
|
3550
|
+
const valuesPath = base + ALL_EXTENSIONS.yaml;
|
|
3551
|
+
const schemaPath = base + ALL_EXTENSIONS.schema;
|
|
3552
|
+
return {
|
|
3553
|
+
form: formPath,
|
|
3554
|
+
report: existsSync(reportPath) ? reportPath : null,
|
|
3555
|
+
values: existsSync(valuesPath) ? valuesPath : null,
|
|
3556
|
+
schema: existsSync(schemaPath) ? schemaPath : null
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Build tabs for tabbed navigation.
|
|
3561
|
+
*/
|
|
3562
|
+
function buildTabs(relatedFiles) {
|
|
3563
|
+
const tabs = [];
|
|
3564
|
+
tabs.push({
|
|
3565
|
+
id: "form",
|
|
3566
|
+
label: "Markform",
|
|
3567
|
+
path: relatedFiles.form
|
|
3568
|
+
});
|
|
3569
|
+
if (relatedFiles.report) tabs.push({
|
|
3570
|
+
id: "report",
|
|
3571
|
+
label: "Report",
|
|
3572
|
+
path: relatedFiles.report
|
|
3573
|
+
});
|
|
3574
|
+
if (relatedFiles.values) tabs.push({
|
|
3575
|
+
id: "values",
|
|
3576
|
+
label: "Values",
|
|
3577
|
+
path: relatedFiles.values
|
|
3578
|
+
});
|
|
3579
|
+
if (relatedFiles.schema) tabs.push({
|
|
3580
|
+
id: "schema",
|
|
3581
|
+
label: "Schema",
|
|
3582
|
+
path: relatedFiles.schema
|
|
3583
|
+
});
|
|
3584
|
+
return tabs;
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3507
3587
|
* Register the serve command.
|
|
3508
3588
|
*/
|
|
3509
3589
|
function registerServeCommand(program) {
|
|
@@ -3517,8 +3597,10 @@ function registerServeCommand(program) {
|
|
|
3517
3597
|
const content = await readFile$1(filePath);
|
|
3518
3598
|
let form = null;
|
|
3519
3599
|
if (fileType === "form") form = parseForm(content);
|
|
3600
|
+
const relatedFiles = fileType === "form" ? findRelatedFiles(filePath) : null;
|
|
3601
|
+
const tabs = relatedFiles ? buildTabs(relatedFiles) : null;
|
|
3520
3602
|
const server = createServer((req, res) => {
|
|
3521
|
-
handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
|
|
3603
|
+
handleRequest(req, res, filePath, fileType, form, ctx, tabs, (updatedForm) => {
|
|
3522
3604
|
form = updatedForm;
|
|
3523
3605
|
}).catch((err) => {
|
|
3524
3606
|
console.error("Request error:", err);
|
|
@@ -3548,10 +3630,29 @@ function registerServeCommand(program) {
|
|
|
3548
3630
|
* Handle HTTP requests.
|
|
3549
3631
|
* Dispatches to appropriate renderer based on file type.
|
|
3550
3632
|
*/
|
|
3551
|
-
async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
|
|
3633
|
+
async function handleRequest(req, res, filePath, fileType, form, ctx, tabs, updateForm) {
|
|
3552
3634
|
const url = req.url ?? "/";
|
|
3635
|
+
if (req.method === "GET" && url.startsWith("/tab/") && tabs && tabs.length > 1) {
|
|
3636
|
+
const tabId = url.slice(5);
|
|
3637
|
+
const tab = tabs.find((t) => t.id === tabId);
|
|
3638
|
+
if (tab?.path) {
|
|
3639
|
+
const content = await readFile(tab.path, "utf-8");
|
|
3640
|
+
const tabFileType = detectFileType(tab.path);
|
|
3641
|
+
let html;
|
|
3642
|
+
if (tabFileType === "report" || tabFileType === "raw") html = renderMarkdownContent(content);
|
|
3643
|
+
else if (tabFileType === "yaml") html = renderYamlContent(content);
|
|
3644
|
+
else if (tabFileType === "json" || tabFileType === "schema") html = renderJsonContent(content);
|
|
3645
|
+
else html = `<pre>${escapeHtml(content)}</pre>`;
|
|
3646
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3647
|
+
res.end(html);
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
res.writeHead(404);
|
|
3651
|
+
res.end("Tab not found");
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3553
3654
|
if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
|
|
3554
|
-
const html = renderFormHtml(form);
|
|
3655
|
+
const html = renderFormHtml(form, tabs);
|
|
3555
3656
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3556
3657
|
res.end(html);
|
|
3557
3658
|
} else if (fileType === "raw" || fileType === "report") {
|
|
@@ -3562,7 +3663,7 @@ async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm
|
|
|
3562
3663
|
const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
3563
3664
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3564
3665
|
res.end(html);
|
|
3565
|
-
} else if (fileType === "json") {
|
|
3666
|
+
} else if (fileType === "json" || fileType === "schema") {
|
|
3566
3667
|
const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
3567
3668
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3568
3669
|
res.end(html);
|
|
@@ -3769,6 +3870,11 @@ function formDataToPatches(formData, form) {
|
|
|
3769
3870
|
});
|
|
3770
3871
|
break;
|
|
3771
3872
|
}
|
|
3873
|
+
case "table": break;
|
|
3874
|
+
default: {
|
|
3875
|
+
const _exhaustive = field;
|
|
3876
|
+
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
3877
|
+
}
|
|
3772
3878
|
}
|
|
3773
3879
|
}
|
|
3774
3880
|
return patches;
|
|
@@ -3814,10 +3920,14 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
|
|
|
3814
3920
|
* Render the form as HTML.
|
|
3815
3921
|
* @public Exported for testing.
|
|
3816
3922
|
*/
|
|
3817
|
-
function renderFormHtml(form) {
|
|
3923
|
+
function renderFormHtml(form, tabs) {
|
|
3818
3924
|
const { schema, responsesByFieldId } = form;
|
|
3819
3925
|
const formTitle = schema.title ?? schema.id;
|
|
3820
3926
|
const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
|
|
3927
|
+
const showTabs = tabs && tabs.length > 1;
|
|
3928
|
+
const tabBarHtml = showTabs ? `<div class="tab-bar">
|
|
3929
|
+
${tabs.map((tab, i) => `<button class="tab-btn${i === 0 ? " active" : ""}" data-tab="${tab.id}">${escapeHtml(tab.label)}</button>`).join("\n ")}
|
|
3930
|
+
</div>` : "";
|
|
3821
3931
|
return `<!DOCTYPE html>
|
|
3822
3932
|
<html lang="en">
|
|
3823
3933
|
<head>
|
|
@@ -3835,7 +3945,74 @@ function renderFormHtml(form) {
|
|
|
3835
3945
|
background: #f8f9fa;
|
|
3836
3946
|
color: #212529;
|
|
3837
3947
|
}
|
|
3838
|
-
h1 { color: #495057; border-bottom:
|
|
3948
|
+
h1 { color: #495057; border-bottom: none; padding-bottom: 0.5rem; }
|
|
3949
|
+
/* Tab bar styles */
|
|
3950
|
+
.tab-bar {
|
|
3951
|
+
display: flex;
|
|
3952
|
+
gap: 0.25rem;
|
|
3953
|
+
margin-bottom: 1.5rem;
|
|
3954
|
+
border-bottom: 2px solid #dee2e6;
|
|
3955
|
+
padding-bottom: 0;
|
|
3956
|
+
}
|
|
3957
|
+
.tab-btn {
|
|
3958
|
+
padding: 0.5rem 1rem;
|
|
3959
|
+
border: none;
|
|
3960
|
+
background: transparent;
|
|
3961
|
+
color: #6c757d;
|
|
3962
|
+
font-size: 0.95rem;
|
|
3963
|
+
cursor: pointer;
|
|
3964
|
+
border-bottom: 2px solid transparent;
|
|
3965
|
+
margin-bottom: -2px;
|
|
3966
|
+
transition: all 0.15s;
|
|
3967
|
+
}
|
|
3968
|
+
.tab-btn:hover {
|
|
3969
|
+
color: #495057;
|
|
3970
|
+
}
|
|
3971
|
+
.tab-btn.active {
|
|
3972
|
+
color: #0d6efd;
|
|
3973
|
+
border-bottom-color: #0d6efd;
|
|
3974
|
+
font-weight: 500;
|
|
3975
|
+
}
|
|
3976
|
+
.tab-content {
|
|
3977
|
+
display: none;
|
|
3978
|
+
}
|
|
3979
|
+
.tab-content.active {
|
|
3980
|
+
display: block;
|
|
3981
|
+
}
|
|
3982
|
+
/* Light theme syntax highlighting for tab content */
|
|
3983
|
+
.tab-content pre {
|
|
3984
|
+
background: #f8f9fa;
|
|
3985
|
+
color: #24292e;
|
|
3986
|
+
padding: 1rem;
|
|
3987
|
+
border-radius: 6px;
|
|
3988
|
+
border: 1px solid #e1e4e8;
|
|
3989
|
+
overflow-x: auto;
|
|
3990
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
3991
|
+
font-size: 0.9rem;
|
|
3992
|
+
line-height: 1.5;
|
|
3993
|
+
}
|
|
3994
|
+
.syn-key { color: #005cc5; }
|
|
3995
|
+
.syn-string { color: #22863a; }
|
|
3996
|
+
.syn-number { color: #005cc5; }
|
|
3997
|
+
.syn-bool { color: #d73a49; }
|
|
3998
|
+
.syn-null { color: #d73a49; }
|
|
3999
|
+
.syn-comment { color: #6a737d; font-style: italic; }
|
|
4000
|
+
/* Markdown content styles */
|
|
4001
|
+
.markdown-content { padding: 0.5rem 0; }
|
|
4002
|
+
.markdown-content h2 { font-size: 1.4rem; color: #24292e; margin: 1.5rem 0 0.75rem; }
|
|
4003
|
+
.markdown-content h3 { font-size: 1.2rem; color: #24292e; margin: 1.25rem 0 0.5rem; }
|
|
4004
|
+
.markdown-content h4 { font-size: 1.1rem; color: #24292e; margin: 1rem 0 0.5rem; }
|
|
4005
|
+
.markdown-content h5 { font-size: 1rem; color: #24292e; margin: 0.75rem 0 0.5rem; }
|
|
4006
|
+
.markdown-content p { margin: 0.75rem 0; line-height: 1.6; }
|
|
4007
|
+
.markdown-content li { margin: 0.25rem 0; margin-left: 1.5rem; line-height: 1.6; }
|
|
4008
|
+
.markdown-content code { background: #f1f3f5; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
|
|
4009
|
+
.markdown-content pre { background: #f8f9fa; padding: 1rem; border-radius: 6px; border: 1px solid #e1e4e8; overflow-x: auto; }
|
|
4010
|
+
.markdown-content pre code { background: none; padding: 0; }
|
|
4011
|
+
.markdown-content a { color: #0366d6; text-decoration: none; }
|
|
4012
|
+
.markdown-content a:hover { text-decoration: underline; }
|
|
4013
|
+
.markdown-content strong { font-weight: 600; }
|
|
4014
|
+
.loading { text-align: center; padding: 2rem; color: #6c757d; }
|
|
4015
|
+
.error { text-align: center; padding: 2rem; color: #dc3545; }
|
|
3839
4016
|
h2 { color: #6c757d; font-size: 1.25rem; }
|
|
3840
4017
|
.group {
|
|
3841
4018
|
background: white;
|
|
@@ -3982,16 +4159,42 @@ function renderFormHtml(form) {
|
|
|
3982
4159
|
border-radius: 3px;
|
|
3983
4160
|
margin-left: 0.5rem;
|
|
3984
4161
|
}
|
|
4162
|
+
.table-container {
|
|
4163
|
+
overflow-x: auto;
|
|
4164
|
+
}
|
|
4165
|
+
.data-table {
|
|
4166
|
+
width: 100%;
|
|
4167
|
+
border-collapse: collapse;
|
|
4168
|
+
font-size: 0.9rem;
|
|
4169
|
+
}
|
|
4170
|
+
.data-table th,
|
|
4171
|
+
.data-table td {
|
|
4172
|
+
padding: 0.5rem 0.75rem;
|
|
4173
|
+
text-align: left;
|
|
4174
|
+
border: 1px solid #dee2e6;
|
|
4175
|
+
}
|
|
4176
|
+
.data-table th {
|
|
4177
|
+
background: #f8f9fa;
|
|
4178
|
+
font-weight: 600;
|
|
4179
|
+
color: #495057;
|
|
4180
|
+
}
|
|
4181
|
+
.data-table tbody tr:hover {
|
|
4182
|
+
background: #f8f9fa;
|
|
4183
|
+
}
|
|
3985
4184
|
</style>
|
|
3986
4185
|
</head>
|
|
3987
4186
|
<body>
|
|
3988
4187
|
<h1>${escapeHtml(formTitle)}</h1>
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
<
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
4188
|
+
${tabBarHtml}
|
|
4189
|
+
<div id="tab-form" class="tab-content active">
|
|
4190
|
+
<form method="POST" action="/save" id="markform">
|
|
4191
|
+
${groupsHtml}
|
|
4192
|
+
<div class="toolbar">
|
|
4193
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
|
4194
|
+
</div>
|
|
4195
|
+
</form>
|
|
4196
|
+
</div>
|
|
4197
|
+
${showTabs ? "<div id=\"tab-other\" class=\"tab-content\"><div class=\"loading\">Loading...</div></div>" : ""}
|
|
3995
4198
|
<script>
|
|
3996
4199
|
// Track fields marked for skip
|
|
3997
4200
|
const skippedFields = new Set();
|
|
@@ -4057,6 +4260,50 @@ function renderFormHtml(form) {
|
|
|
4057
4260
|
alert('Save failed: ' + err.message);
|
|
4058
4261
|
}
|
|
4059
4262
|
});
|
|
4263
|
+
|
|
4264
|
+
// Tab switching logic
|
|
4265
|
+
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
4266
|
+
const tabFormContent = document.getElementById('tab-form');
|
|
4267
|
+
const tabOtherContent = document.getElementById('tab-other');
|
|
4268
|
+
const tabCache = {};
|
|
4269
|
+
|
|
4270
|
+
tabButtons.forEach(btn => {
|
|
4271
|
+
btn.addEventListener('click', async () => {
|
|
4272
|
+
const tabId = btn.dataset.tab;
|
|
4273
|
+
|
|
4274
|
+
// Update active button
|
|
4275
|
+
tabButtons.forEach(b => b.classList.remove('active'));
|
|
4276
|
+
btn.classList.add('active');
|
|
4277
|
+
|
|
4278
|
+
if (tabId === 'form') {
|
|
4279
|
+
// Show form tab
|
|
4280
|
+
if (tabFormContent) tabFormContent.classList.add('active');
|
|
4281
|
+
if (tabOtherContent) tabOtherContent.classList.remove('active');
|
|
4282
|
+
} else {
|
|
4283
|
+
// Show other tab content
|
|
4284
|
+
if (tabFormContent) tabFormContent.classList.remove('active');
|
|
4285
|
+
if (tabOtherContent) {
|
|
4286
|
+
tabOtherContent.classList.add('active');
|
|
4287
|
+
|
|
4288
|
+
// Fetch content if not cached
|
|
4289
|
+
if (!tabCache[tabId]) {
|
|
4290
|
+
tabOtherContent.innerHTML = '<div class="loading">Loading...</div>';
|
|
4291
|
+
try {
|
|
4292
|
+
const response = await fetch('/tab/' + tabId);
|
|
4293
|
+
if (response.ok) {
|
|
4294
|
+
tabCache[tabId] = await response.text();
|
|
4295
|
+
} else {
|
|
4296
|
+
tabCache[tabId] = '<div class="error">Failed to load content</div>';
|
|
4297
|
+
}
|
|
4298
|
+
} catch (err) {
|
|
4299
|
+
tabCache[tabId] = '<div class="error">Failed to load content</div>';
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
tabOtherContent.innerHTML = tabCache[tabId];
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
});
|
|
4306
|
+
});
|
|
4060
4307
|
<\/script>
|
|
4061
4308
|
</body>
|
|
4062
4309
|
</html>`;
|
|
@@ -4119,7 +4366,13 @@ function renderFieldHtml(field, value, isSkipped) {
|
|
|
4119
4366
|
case "year":
|
|
4120
4367
|
inputHtml = renderYearInput(field, value, disabledAttr);
|
|
4121
4368
|
break;
|
|
4122
|
-
|
|
4369
|
+
case "table":
|
|
4370
|
+
inputHtml = renderTableInput(field, value, disabledAttr);
|
|
4371
|
+
break;
|
|
4372
|
+
default: {
|
|
4373
|
+
const _exhaustive = field;
|
|
4374
|
+
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
4375
|
+
}
|
|
4123
4376
|
}
|
|
4124
4377
|
const skipButton = !field.required && !skipped ? `<div class="field-actions">
|
|
4125
4378
|
<button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
|
|
@@ -4284,6 +4537,34 @@ function renderCheckboxesInput(field, value, disabledAttr) {
|
|
|
4284
4537
|
</div>`;
|
|
4285
4538
|
}
|
|
4286
4539
|
/**
|
|
4540
|
+
* Render a table field as an HTML table.
|
|
4541
|
+
* Currently read-only display; editing requires more complex UI.
|
|
4542
|
+
*/
|
|
4543
|
+
function renderTableInput(field, value, _disabledAttr) {
|
|
4544
|
+
const rows = value?.rows ?? [];
|
|
4545
|
+
if (rows.length === 0) return "<div class=\"field-help\">(no data)</div>";
|
|
4546
|
+
return `<div class="table-container">
|
|
4547
|
+
<table class="data-table">
|
|
4548
|
+
<thead>
|
|
4549
|
+
<tr>${field.columns.map((col) => `<th>${escapeHtml(col.label)}</th>`).join("")}</tr>
|
|
4550
|
+
</thead>
|
|
4551
|
+
<tbody>
|
|
4552
|
+
${rows.map((row) => {
|
|
4553
|
+
return `<tr>${field.columns.map((col) => {
|
|
4554
|
+
const cell = row[col.id];
|
|
4555
|
+
let cellValue = "";
|
|
4556
|
+
if (cell?.state === "answered" && cell.value !== void 0 && cell.value !== null) cellValue = String(cell.value);
|
|
4557
|
+
else if (cell?.state === "skipped") cellValue = cell.reason ? `[skipped: ${cell.reason}]` : "[skipped]";
|
|
4558
|
+
else if (cell?.state === "aborted") cellValue = cell.reason ? `[aborted: ${cell.reason}]` : "[aborted]";
|
|
4559
|
+
return `<td>${escapeHtml(cellValue)}</td>`;
|
|
4560
|
+
}).join("")}</tr>`;
|
|
4561
|
+
}).join("\n ")}
|
|
4562
|
+
</tbody>
|
|
4563
|
+
</table>
|
|
4564
|
+
<div class="field-help">(table fields are currently read-only in the web UI)</div>
|
|
4565
|
+
</div>`;
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4287
4568
|
* Escape HTML special characters.
|
|
4288
4569
|
* @public Exported for testing.
|
|
4289
4570
|
*/
|
|
@@ -4310,15 +4591,23 @@ const READ_ONLY_STYLES = `
|
|
|
4310
4591
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
4311
4592
|
}
|
|
4312
4593
|
pre {
|
|
4313
|
-
background: #
|
|
4314
|
-
color: #
|
|
4594
|
+
background: #f8f9fa;
|
|
4595
|
+
color: #24292e;
|
|
4315
4596
|
padding: 1rem;
|
|
4316
4597
|
border-radius: 6px;
|
|
4598
|
+
border: 1px solid #e1e4e8;
|
|
4317
4599
|
overflow-x: auto;
|
|
4318
4600
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
4319
4601
|
font-size: 0.9rem;
|
|
4320
4602
|
line-height: 1.5;
|
|
4321
4603
|
}
|
|
4604
|
+
/* Light theme syntax highlighting */
|
|
4605
|
+
.syn-key { color: #005cc5; }
|
|
4606
|
+
.syn-string { color: #22863a; }
|
|
4607
|
+
.syn-number { color: #005cc5; }
|
|
4608
|
+
.syn-bool { color: #d73a49; }
|
|
4609
|
+
.syn-null { color: #d73a49; }
|
|
4610
|
+
.syn-comment { color: #6a737d; font-style: italic; }
|
|
4322
4611
|
.badge {
|
|
4323
4612
|
font-size: 0.75rem;
|
|
4324
4613
|
padding: 0.2rem 0.5rem;
|
|
@@ -4397,9 +4686,23 @@ function renderMarkdownHtml(content, filename) {
|
|
|
4397
4686
|
*/
|
|
4398
4687
|
function renderYamlHtml(content, filename) {
|
|
4399
4688
|
const highlighted = content.split("\n").map((line) => {
|
|
4689
|
+
if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
|
|
4400
4690
|
const colonIndex = line.indexOf(":");
|
|
4401
|
-
if (colonIndex > 0 && !line.trim().startsWith("
|
|
4402
|
-
|
|
4691
|
+
if (colonIndex > 0 && !line.trim().startsWith("-")) {
|
|
4692
|
+
const key = escapeHtml(line.slice(0, colonIndex));
|
|
4693
|
+
const afterColon = line.slice(colonIndex + 1).trim();
|
|
4694
|
+
const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
|
|
4695
|
+
if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
|
|
4696
|
+
const valueStart = line.indexOf(afterColon, colonIndex);
|
|
4697
|
+
return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
|
|
4698
|
+
}
|
|
4699
|
+
if (line.trim().startsWith("-")) {
|
|
4700
|
+
const dashIndex = line.indexOf("-");
|
|
4701
|
+
const beforeDash = escapeHtml(line.slice(0, dashIndex));
|
|
4702
|
+
const afterDash = line.slice(dashIndex + 1).trim();
|
|
4703
|
+
if (afterDash === "") return `${beforeDash}-`;
|
|
4704
|
+
return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
|
|
4705
|
+
}
|
|
4403
4706
|
return escapeHtml(line);
|
|
4404
4707
|
}).join("\n");
|
|
4405
4708
|
return `<!DOCTYPE html>
|
|
@@ -4419,6 +4722,17 @@ function renderYamlHtml(content, filename) {
|
|
|
4419
4722
|
</html>`;
|
|
4420
4723
|
}
|
|
4421
4724
|
/**
|
|
4725
|
+
* Highlight a YAML value based on its type.
|
|
4726
|
+
*/
|
|
4727
|
+
function highlightYamlValue(value) {
|
|
4728
|
+
const trimmed = value.trim();
|
|
4729
|
+
if (trimmed === "true" || trimmed === "false") return `<span class="syn-bool">${escapeHtml(value)}</span>`;
|
|
4730
|
+
if (trimmed === "null" || trimmed === "~") return `<span class="syn-null">${escapeHtml(value)}</span>`;
|
|
4731
|
+
if (/^-?\d+\.?\d*$/.test(trimmed)) return `<span class="syn-number">${escapeHtml(value)}</span>`;
|
|
4732
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
4733
|
+
return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
4734
|
+
}
|
|
4735
|
+
/**
|
|
4422
4736
|
* Render JSON content with syntax highlighting and formatting.
|
|
4423
4737
|
*/
|
|
4424
4738
|
function renderJsonHtml(content, filename) {
|
|
@@ -4429,7 +4743,7 @@ function renderJsonHtml(content, filename) {
|
|
|
4429
4743
|
} catch {
|
|
4430
4744
|
formatted = content;
|
|
4431
4745
|
}
|
|
4432
|
-
const highlighted = formatted.replace(/"([^"]+)":/g, "<span
|
|
4746
|
+
const highlighted = 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>");
|
|
4433
4747
|
return `<!DOCTYPE html>
|
|
4434
4748
|
<html lang="en">
|
|
4435
4749
|
<head>
|
|
@@ -4466,6 +4780,144 @@ function renderPlainTextHtml(content, filename) {
|
|
|
4466
4780
|
</body>
|
|
4467
4781
|
</html>`;
|
|
4468
4782
|
}
|
|
4783
|
+
/**
|
|
4784
|
+
* Render markdown content (content only, no page wrapper).
|
|
4785
|
+
* Used for tab content.
|
|
4786
|
+
* @public Exported for testing.
|
|
4787
|
+
*/
|
|
4788
|
+
function renderMarkdownContent(content) {
|
|
4789
|
+
const lines = content.split("\n");
|
|
4790
|
+
let html = "<div class=\"markdown-content\">";
|
|
4791
|
+
let inParagraph = false;
|
|
4792
|
+
let inCodeBlock = false;
|
|
4793
|
+
let codeBlockContent = "";
|
|
4794
|
+
for (const line of lines) {
|
|
4795
|
+
const trimmed = line.trim();
|
|
4796
|
+
if (trimmed.startsWith("```")) {
|
|
4797
|
+
if (inCodeBlock) {
|
|
4798
|
+
html += `<pre><code>${escapeHtml(codeBlockContent.trim())}</code></pre>`;
|
|
4799
|
+
codeBlockContent = "";
|
|
4800
|
+
inCodeBlock = false;
|
|
4801
|
+
} else {
|
|
4802
|
+
if (inParagraph) {
|
|
4803
|
+
html += "</p>";
|
|
4804
|
+
inParagraph = false;
|
|
4805
|
+
}
|
|
4806
|
+
inCodeBlock = true;
|
|
4807
|
+
}
|
|
4808
|
+
continue;
|
|
4809
|
+
}
|
|
4810
|
+
if (inCodeBlock) {
|
|
4811
|
+
codeBlockContent += line + "\n";
|
|
4812
|
+
continue;
|
|
4813
|
+
}
|
|
4814
|
+
if (trimmed.startsWith("# ")) {
|
|
4815
|
+
if (inParagraph) {
|
|
4816
|
+
html += "</p>";
|
|
4817
|
+
inParagraph = false;
|
|
4818
|
+
}
|
|
4819
|
+
html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
|
|
4820
|
+
} else if (trimmed.startsWith("## ")) {
|
|
4821
|
+
if (inParagraph) {
|
|
4822
|
+
html += "</p>";
|
|
4823
|
+
inParagraph = false;
|
|
4824
|
+
}
|
|
4825
|
+
html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
|
|
4826
|
+
} else if (trimmed.startsWith("### ")) {
|
|
4827
|
+
if (inParagraph) {
|
|
4828
|
+
html += "</p>";
|
|
4829
|
+
inParagraph = false;
|
|
4830
|
+
}
|
|
4831
|
+
html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
|
|
4832
|
+
} else if (trimmed.startsWith("#### ")) {
|
|
4833
|
+
if (inParagraph) {
|
|
4834
|
+
html += "</p>";
|
|
4835
|
+
inParagraph = false;
|
|
4836
|
+
}
|
|
4837
|
+
html += `<h5>${escapeHtml(trimmed.slice(5))}</h5>`;
|
|
4838
|
+
} else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
|
|
4839
|
+
if (inParagraph) {
|
|
4840
|
+
html += "</p>";
|
|
4841
|
+
inParagraph = false;
|
|
4842
|
+
}
|
|
4843
|
+
html += `<li>${formatInlineMarkdown(trimmed.slice(2))}</li>`;
|
|
4844
|
+
} else if (/^\d+\.\s/.test(trimmed)) {
|
|
4845
|
+
if (inParagraph) {
|
|
4846
|
+
html += "</p>";
|
|
4847
|
+
inParagraph = false;
|
|
4848
|
+
}
|
|
4849
|
+
const text = trimmed.replace(/^\d+\.\s/, "");
|
|
4850
|
+
html += `<li>${formatInlineMarkdown(text)}</li>`;
|
|
4851
|
+
} else if (trimmed === "") {
|
|
4852
|
+
if (inParagraph) {
|
|
4853
|
+
html += "</p>";
|
|
4854
|
+
inParagraph = false;
|
|
4855
|
+
}
|
|
4856
|
+
} else {
|
|
4857
|
+
if (!inParagraph) {
|
|
4858
|
+
html += "<p>";
|
|
4859
|
+
inParagraph = true;
|
|
4860
|
+
} else html += "<br>";
|
|
4861
|
+
html += formatInlineMarkdown(trimmed);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
if (inParagraph) html += "</p>";
|
|
4865
|
+
html += "</div>";
|
|
4866
|
+
return html;
|
|
4867
|
+
}
|
|
4868
|
+
/**
|
|
4869
|
+
* Format inline markdown (bold, italic, code, links).
|
|
4870
|
+
*/
|
|
4871
|
+
function formatInlineMarkdown(text) {
|
|
4872
|
+
let result = escapeHtml(text);
|
|
4873
|
+
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
4874
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
4875
|
+
result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
4876
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => `<a href="${url.replace(/&/g, "&")}" target="_blank">${linkText}</a>`);
|
|
4877
|
+
return result;
|
|
4878
|
+
}
|
|
4879
|
+
/**
|
|
4880
|
+
* Render YAML content (content only, no page wrapper).
|
|
4881
|
+
* Used for tab content.
|
|
4882
|
+
* @public Exported for testing.
|
|
4883
|
+
*/
|
|
4884
|
+
function renderYamlContent(content) {
|
|
4885
|
+
return `<pre>${content.split("\n").map((line) => {
|
|
4886
|
+
if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
|
|
4887
|
+
const colonIndex = line.indexOf(":");
|
|
4888
|
+
if (colonIndex > 0 && !line.trim().startsWith("-")) {
|
|
4889
|
+
const key = escapeHtml(line.slice(0, colonIndex));
|
|
4890
|
+
const afterColon = line.slice(colonIndex + 1).trim();
|
|
4891
|
+
const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
|
|
4892
|
+
if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
|
|
4893
|
+
const valueStart = line.indexOf(afterColon, colonIndex);
|
|
4894
|
+
return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
|
|
4895
|
+
}
|
|
4896
|
+
if (line.trim().startsWith("-")) {
|
|
4897
|
+
const dashIndex = line.indexOf("-");
|
|
4898
|
+
const beforeDash = escapeHtml(line.slice(0, dashIndex));
|
|
4899
|
+
const afterDash = line.slice(dashIndex + 1).trim();
|
|
4900
|
+
if (afterDash === "") return `${beforeDash}-`;
|
|
4901
|
+
return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
|
|
4902
|
+
}
|
|
4903
|
+
return escapeHtml(line);
|
|
4904
|
+
}).join("\n")}</pre>`;
|
|
4905
|
+
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Render JSON content (content only, no page wrapper).
|
|
4908
|
+
* Used for tab content.
|
|
4909
|
+
* @public Exported for testing.
|
|
4910
|
+
*/
|
|
4911
|
+
function renderJsonContent(content) {
|
|
4912
|
+
let formatted;
|
|
4913
|
+
try {
|
|
4914
|
+
const parsed = JSON.parse(content);
|
|
4915
|
+
formatted = JSON.stringify(parsed, null, 2);
|
|
4916
|
+
} catch {
|
|
4917
|
+
formatted = content;
|
|
4918
|
+
}
|
|
4919
|
+
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>`;
|
|
4920
|
+
}
|
|
4469
4921
|
|
|
4470
4922
|
//#endregion
|
|
4471
4923
|
//#region src/cli/commands/render.ts
|
|
@@ -4636,6 +5088,7 @@ function registerResearchCommand(program) {
|
|
|
4636
5088
|
result = await runResearch(form, {
|
|
4637
5089
|
model: modelId,
|
|
4638
5090
|
enableWebSearch: true,
|
|
5091
|
+
captureWireFormat: false,
|
|
4639
5092
|
maxTurns,
|
|
4640
5093
|
maxPatchesPerTurn,
|
|
4641
5094
|
maxIssuesPerTurn,
|
|
@@ -4658,9 +5111,9 @@ function registerResearchCommand(program) {
|
|
|
4658
5111
|
console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
|
|
4659
5112
|
console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
|
|
4660
5113
|
if (options.transcript && result.transcript) {
|
|
4661
|
-
const { serializeSession: serializeSession$1 } = await import("./session-
|
|
5114
|
+
const { serializeSession: serializeSession$1 } = await import("./session-CzCh6JeY.mjs");
|
|
4662
5115
|
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
4663
|
-
const { writeFile: writeFile$1 } = await import("./shared-
|
|
5116
|
+
const { writeFile: writeFile$1 } = await import("./shared-CNqwaxUt.mjs");
|
|
4664
5117
|
await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
|
|
4665
5118
|
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
4666
5119
|
}
|