Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Usage:
termchart lint [file] Check source is within the renderable subset
termchart serve [flags] Start a local viewer + print its URL and env exports (--port --token --wsid --bucket)
termchart begin [flags] Show a scope as "composing" instantly — placeholder + focus + loader in one call (--project --agent --description)
termchart push [flags] Push a diagram to the remote viewer (--project --agent --type --description --focus [--schema <file> to make it template-able])
termchart push [flags] Push a rich diagram to the remote viewer (--project --agent --type --description --focus [--schema <file> to make it template-able])
termchart status [flags] Push a status update — toast/progress/loader (--message --progress --loader --id --done)
termchart clear [flags] Clear viewer scopes (--project --agent for one, or --all)
termchart pull [flags] Fetch a stored view's spec to iterate on (--project --agent [--json])
Expand All @@ -45,6 +45,14 @@ Usage:
termchart run <id> Refresh a scheduled board now — enforce lifecycle, assemble its prompt + run its agent, which pushes the board (--timeout <secs>, default 600)
termchart --version

Push --type (rich viewer types — these are NOT Mermaid diagram keywords):
component comparisons / product cards / status boards / dashboards (Mantine UI tree)
flow architecture / sequence / ER / class / state machine / pipeline (nodes + edges)
vegalite charts — metrics / trends / distributions (a Vega-Lite spec)
panes a grid or rows of several of the above (multi-tile dashboards)
calltree call hierarchies datatable large tabular data
markdown prose / notes / report mermaid quick throwaway flowchart (ASCII fallback)

Render flags:
--ascii Pure ASCII (+,-,|) instead of Unicode box-drawing
--safe, --portable Alias for --ascii — guaranteed alignment in any terminal
Expand Down
34 changes: 32 additions & 2 deletions packages/core/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ export type KnownPushType = (typeof KNOWN_PUSH_TYPES)[number];
const KNOWN_TYPE_SET = new Set<string>(KNOWN_PUSH_TYPES);
const KNOWN_PUSH_TYPES_LIST = KNOWN_PUSH_TYPES.join(", ");

// Common wrong `--type` values agents reach for — Mermaid diagram keywords ("flowchart",
// "erDiagram") and semantic names ("comparison", "architecture", "line") — mapped to the
// termchart type that actually renders that intent. Surfaced as a "did you mean" in the reject
// message so a pushing agent self-corrects to the RIGHT journey, not merely a renderable type.
const TYPE_ALIASES: Record<string, KnownPushType> = {
comparison: "component", compare: "component", cards: "component", card: "component",
status: "component", statusboard: "component", stats: "component", scorecard: "component",
dashboard: "panes", grid: "panes",
flowchart: "flow", graph: "flow", digraph: "flow", architecture: "flow", diagram: "flow",
sequence: "flow", sequencediagram: "flow", state: "flow", statemachine: "flow", statediagram: "flow",
erd: "flow", er: "flow", erdiagram: "flow", schema: "flow", sql: "flow", class: "flow",
classdiagram: "flow", pipeline: "flow", tree: "flow", network: "flow",
line: "vegalite", bar: "vegalite", chart: "vegalite", plot: "vegalite", scatter: "vegalite",
histogram: "vegalite", area: "vegalite", pie: "vegalite", timeseries: "vegalite",
metric: "vegalite", metrics: "vegalite", trend: "vegalite",
callgraph: "calltree", callhierarchy: "calltree",
table: "datatable", datatabel: "datatable", spreadsheet: "datatable",
md: "markdown", doc: "markdown", document: "markdown", report: "markdown", notes: "markdown", text: "markdown",
};
// One-line intent -> type guide appended to the reject message (kept ASCII).
const TYPE_INTENT_GUIDE =
"pick by intent: comparisons / product cards / status -> component; dashboards of peer tiles -> panes; " +
"architecture / flow / sequence / ER / class / state / pipeline -> flow; " +
"charts / metrics / trends / distributions -> vegalite; call hierarchies -> calltree; " +
"large tabular data -> datatable; prose / notes -> markdown";

/**
* The client component-resolver's whitelist (component-resolver.tsx REGISTRY). A node with any
* other `type` renders as a visible "[unknown component: X]" marker — observed in the wild:
Expand Down Expand Up @@ -300,11 +326,15 @@ const STRUCTURED_TYPES = new Set(["component", "panes", "flow", "vegalite", "cal
export function validateContent(type: string, content: string, depth = 0): string | null {
// The viewer can only display types it has a renderer for — anything else would be stored,
// 204'd, and then break in the browser with no feedback to the pusher.
if (!KNOWN_TYPE_SET.has(type))
if (!KNOWN_TYPE_SET.has(type)) {
const guess = TYPE_ALIASES[type.toLowerCase().replace(/[^a-z]/g, "")];
return (
`unknown type "${type}" — the viewer has no renderer for it and would show a ` +
`"[unsupported type]" block. Supported types: ${KNOWN_PUSH_TYPES_LIST}`
`"[unsupported type]" block.` +
(guess ? ` Did you mean "${guess}"?` : "") +
` Supported types: ${KNOWN_PUSH_TYPES_LIST}. To ${TYPE_INTENT_GUIDE}`
);
}
// mermaid is NOT freeform — validate against the renderable subset (header + supported constructs).
if (type === "mermaid") return mermaidErrors(content)[0] ?? null;
if (!STRUCTURED_TYPES.has(type)) return null; // text/ansi/markdown are genuinely freeform
Expand Down
17 changes: 17 additions & 0 deletions packages/viewer/test/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ describe("validateContent", () => {
expect(validateContent("flow", JSON.stringify({ nodes: [], edges: [] }))).toBeNull();
});

it("maps a mistaken --type to the right one (did-you-mean) so an agent self-corrects to the correct journey", () => {
// Mermaid keywords + semantic names agents reach for -> the type that renders that intent.
const cases: Array<[string, string]> = [
["comparison", "component"], ["erDiagram", "flow"], ["flowchart", "flow"], ["sql", "flow"],
["line", "vegalite"], ["metrics", "vegalite"], ["status", "component"], ["dashboard", "panes"],
["table", "datatable"],
];
for (const [bad, want] of cases) {
const msg = validateContent(bad, "{}") ?? "";
expect(msg).toContain(`unknown type "${bad}"`);
expect(msg).toContain(`Did you mean "${want}"`);
expect(msg).toContain("pick by intent"); // the intent guide is always appended
}
// a truly unrecognizable type still errors, just without a guess
expect(validateContent("zzzzz", "{}")).toContain('unknown type "zzzzz"');
});

it("rejects a null/primitive flow node or edge with a path (would otherwise 500 in the renderer)", () => {
expect(validateContent("flow", JSON.stringify({ nodes: [null] }))).toBe("flow.nodes[0] must be an object");
expect(validateContent("flow", JSON.stringify({ nodes: [{ id: "a" }, "x"] }))).toBe("flow.nodes[1] must be an object");
Expand Down
Loading