mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Add syntax-highlighted DOT graph display below TOML config
Add a custom Shiki TextMate grammar for GraphViz DOT files (converted from vscode-graphviz) and display each workflow's DOT graph source below the TOML configuration on the definition tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bc1b5d54ab
commit
b218c6b280
3 changed files with 415 additions and 3 deletions
106
apps/arc-web/app/data/dot-grammar.ts
Normal file
106
apps/arc-web/app/data/dot-grammar.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import type { LanguageRegistration } from "shiki";
|
||||
|
||||
export const dotLanguage: LanguageRegistration = {
|
||||
name: "dot",
|
||||
scopeName: "source.dot",
|
||||
fileTypes: ["dot", "DOT", "gv"],
|
||||
firstLineMatch: "digraph.*",
|
||||
patterns: [
|
||||
{
|
||||
match: " ?(digraph)[ \\t]+([A-Za-z0-9]+) ?(\\{)",
|
||||
captures: {
|
||||
"1": { name: "storage.type.dot" },
|
||||
"2": { name: "variable.other.dot" },
|
||||
"3": { name: "punctuation.section.dot" },
|
||||
},
|
||||
},
|
||||
{
|
||||
match: "(<|-)(>|-)",
|
||||
name: "keyword.operator.dot",
|
||||
},
|
||||
{
|
||||
match: "\\b(node|edge|graph|digraph|subgraph|strict)\\b",
|
||||
name: "storage.type.dot",
|
||||
},
|
||||
{
|
||||
match:
|
||||
"\\b(bottomlabel|color|comment|distortion|fillcolor|fixedsize|fontcolor|fontname|fontsize|group|height|label|layer|orientation|peripheries|regular|shape|shapefile|sides|skew|style|toplabel|URL|width|z)\\b",
|
||||
name: "support.constant.attribute.node.dot",
|
||||
},
|
||||
{
|
||||
match:
|
||||
"\\b(arrowhead|arrowsize|arrowtail|color|comment|constraint|decorate|dir|fontcolor|fontname|fontsize|headlabel|headport|headURL|label|labelangle|labeldistance|labelfloat|labelcolor|labelfontname|labelfontsize|layer|lhead|ltail|minlen|samehead|sametail|splines|style|taillabel|tailport|tailURL|weight)\\b",
|
||||
name: "support.constant.attribute.edge.dot",
|
||||
},
|
||||
{
|
||||
match:
|
||||
"\\b(bgcolor|center|clusterrank|color|comment|compound|concentrate|fillcolor|fontname|fontpath|fontsize|label|labeljust|labelloc|layers|margin|mclimit|nodesep|nslimit|nslimit1|ordering|orientation|page|pagedir|quantum|rank|rankdir|ranksep|ratio|remincross|rotate|samplepoints|searchsize|size|style|URL)\\b",
|
||||
name: "support.constant.attribute.graph.dot",
|
||||
},
|
||||
{
|
||||
match:
|
||||
"\\b(box|polygon|ellipse|circle|point|egg|triangle|plaintext|diamond|trapezium|parallelogram|house|pentagon|hexagon|septagon|octagon|doublecircle|doubleoctagon|tripleoctagon|invtriangle|invtrapezium|invhouse|Mdiamond|Msquare|Mcircle|rect|rectangle|none|note|tab|folder|box3d|component|max|min|same)\\b",
|
||||
name: "variable.other.dot",
|
||||
},
|
||||
{
|
||||
begin: '"',
|
||||
beginCaptures: {
|
||||
"0": { name: "punctuation.definition.string.begin.dot" },
|
||||
},
|
||||
end: '"',
|
||||
endCaptures: {
|
||||
"0": { name: "punctuation.definition.string.end.dot" },
|
||||
},
|
||||
name: "string.quoted.double.dot",
|
||||
patterns: [
|
||||
{
|
||||
match: "\\\\.",
|
||||
name: "constant.character.escape.dot",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
begin: "(^[ \\t]+)?(?=//)",
|
||||
beginCaptures: {
|
||||
"1": { name: "punctuation.whitespace.comment.leading.dot" },
|
||||
},
|
||||
end: "(?!\\G)",
|
||||
patterns: [
|
||||
{
|
||||
begin: "//",
|
||||
beginCaptures: {
|
||||
"0": { name: "punctuation.definition.comment.dot" },
|
||||
},
|
||||
end: "\\n",
|
||||
name: "comment.line.double-slash.dot",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
begin: "(^[ \\t]+)?(?=#)",
|
||||
beginCaptures: {
|
||||
"1": { name: "punctuation.whitespace.comment.leading.dot" },
|
||||
},
|
||||
end: "(?!\\G)",
|
||||
patterns: [
|
||||
{
|
||||
begin: "#",
|
||||
beginCaptures: {
|
||||
"0": { name: "punctuation.definition.comment.dot" },
|
||||
},
|
||||
end: "\\n",
|
||||
name: "comment.line.number-sign.dot",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
begin: "/\\*",
|
||||
captures: {
|
||||
"0": { name: "punctuation.definition.comment.dot" },
|
||||
},
|
||||
end: "\\*/",
|
||||
name: "comment.block.dot",
|
||||
},
|
||||
],
|
||||
repository: {},
|
||||
};
|
||||
|
|
@ -1,3 +1,82 @@
|
|||
export default function WorkflowDefinition() {
|
||||
return <p className="text-sm text-navy-600">Workflow definition will appear here.</p>;
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { workflowData } from "./workflow-detail";
|
||||
import { dotLanguage } from "../data/dot-grammar";
|
||||
|
||||
function CodeBlock({
|
||||
code,
|
||||
lang,
|
||||
filename,
|
||||
}: {
|
||||
code: string;
|
||||
lang: string;
|
||||
filename: string;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function highlight() {
|
||||
const { createHighlighter } = await import("shiki");
|
||||
if (cancelled) return;
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ["nord"],
|
||||
langs: lang === "dot" ? [dotLanguage] : [lang],
|
||||
});
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const html = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: "nord",
|
||||
});
|
||||
|
||||
if (cancelled || containerRef.current == null) return;
|
||||
containerRef.current.innerHTML = html;
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
highlight();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [code, lang]);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-white/[0.06] bg-navy-800/50 overflow-hidden">
|
||||
<div className="flex items-center gap-2 border-b border-white/[0.06] px-4 py-2.5">
|
||||
<span className="font-mono text-xs text-navy-600">{filename}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`shiki-container overflow-x-auto transition-opacity duration-200 ${ready ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
|
||||
{!ready && (
|
||||
<pre className="px-4 py-4 font-mono text-sm leading-relaxed text-navy-600">
|
||||
{code}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkflowDefinition() {
|
||||
const { name } = useParams();
|
||||
const workflow = workflowData[name ?? ""];
|
||||
|
||||
if (workflow == null) {
|
||||
return <p className="text-sm text-navy-600">No configuration found.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<CodeBlock code={workflow.config} lang="toml" filename="task.toml" />
|
||||
<CodeBlock code={workflow.graph} lang="dot" filename={workflow.filename} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,253 @@
|
|||
import { Link, Outlet, useLocation, useParams } from "react-router";
|
||||
import type { Route } from "./+types/workflow-detail";
|
||||
|
||||
export const workflowData: Record<string, { title: string; description: string; filename: string }> = {
|
||||
interface WorkflowEntry {
|
||||
title: string;
|
||||
description: string;
|
||||
filename: string;
|
||||
config: string;
|
||||
graph: string;
|
||||
}
|
||||
|
||||
export const workflowData: Record<string, WorkflowEntry> = {
|
||||
fix_build: {
|
||||
title: "Fix Build",
|
||||
filename: "fix_build.dot",
|
||||
description: "Automatically diagnoses and fixes CI build failures by analyzing error logs, identifying root causes, and applying targeted code changes.",
|
||||
config: `version = 1
|
||||
task = "Diagnose and fix CI build failures"
|
||||
graph = "fix_build.dot"
|
||||
|
||||
[llm]
|
||||
model = "claude-sonnet"
|
||||
|
||||
[vars]
|
||||
repo_url = "https://github.com/org/service"
|
||||
branch = "main"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
auto_stop_interval = 60
|
||||
|
||||
[execution.daytona.sandbox.labels]
|
||||
project = "fix-build"
|
||||
|
||||
[execution.daytona.snapshot]
|
||||
name = "fix-build-dev"
|
||||
cpu = 4
|
||||
memory = 8
|
||||
disk = 10
|
||||
`,
|
||||
graph: `digraph fix_build {
|
||||
graph [
|
||||
goal="Diagnose and fix CI build failures",
|
||||
label="Fix Build"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
diagnose [label="Diagnose Failure", prompt="@prompts/fix_build/diagnose.md", reasoning_effort="high"]
|
||||
fix [label="Apply Fix", prompt="@prompts/fix_build/fix.md"]
|
||||
validate [label="Run Build", prompt="@prompts/fix_build/validate.md", goal_gate=true]
|
||||
gate [shape=diamond, label="Build passing?"]
|
||||
|
||||
start -> diagnose -> fix -> validate -> gate
|
||||
gate -> exit [label="Yes", condition="outcome=success"]
|
||||
gate -> diagnose [label="No", condition="outcome!=success", max_visits=3]
|
||||
}
|
||||
`,
|
||||
},
|
||||
implement: {
|
||||
title: "Implement Feature",
|
||||
filename: "implement.dot",
|
||||
description: "Generates production-ready code from a technical blueprint, including tests, documentation, and a pull request ready for review.",
|
||||
config: `version = 1
|
||||
task = "Implement feature from technical blueprint"
|
||||
graph = "implement.dot"
|
||||
|
||||
[llm]
|
||||
model = "claude-sonnet"
|
||||
|
||||
[vars]
|
||||
spec_path = "specs/feature.md"
|
||||
test_framework = "vitest"
|
||||
|
||||
[setup]
|
||||
commands = ["bun install", "bun run typecheck"]
|
||||
timeout_ms = 120000
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
auto_stop_interval = 120
|
||||
|
||||
[execution.daytona.sandbox.labels]
|
||||
project = "implement"
|
||||
team = "engineering"
|
||||
|
||||
[execution.daytona.snapshot]
|
||||
name = "implement-dev"
|
||||
cpu = 4
|
||||
memory = 8
|
||||
disk = 20
|
||||
`,
|
||||
graph: `digraph implement {
|
||||
graph [
|
||||
goal="",
|
||||
label="Implement"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
strategy [shape=hexagon, label="Choose decomposition strategy:"]
|
||||
|
||||
subgraph cluster_impl {
|
||||
label="Implementation Loop"
|
||||
node [fidelity="full", thread_id="impl"]
|
||||
|
||||
plan [label="Plan Implementation", prompt="@prompts/implement/plan.md", reasoning_effort="high"]
|
||||
implement [label="Implement", prompt="@prompts/implement/implement.md"]
|
||||
review [label="Review", prompt="@prompts/implement/review.md"]
|
||||
validate [label="Validate", prompt="@prompts/implement/validate.md", goal_gate=true]
|
||||
fix [label="Fix Failures", prompt="@prompts/implement/fix.md", max_visits=3]
|
||||
}
|
||||
|
||||
start -> strategy
|
||||
strategy -> plan [label="[L] Layer-by-layer"]
|
||||
strategy -> plan [label="[F] Feature slice"]
|
||||
strategy -> plan [label="[P] Embarrassingly parallel"]
|
||||
strategy -> plan [label="[S] Sequential / linear"]
|
||||
plan -> implement -> review -> validate
|
||||
validate -> exit [condition="outcome=success"]
|
||||
validate -> fix [condition="outcome!=success", label="Fix"]
|
||||
fix -> validate
|
||||
}
|
||||
`,
|
||||
},
|
||||
sync_drift: {
|
||||
title: "Sync Drift",
|
||||
filename: "sync_drift.dot",
|
||||
description: "Detects configuration and code drift between environments, then generates reconciliation patches to bring everything back in sync.",
|
||||
config: `version = 1
|
||||
task = "Detect and reconcile configuration drift across environments"
|
||||
graph = "sync_drift.dot"
|
||||
|
||||
[llm]
|
||||
model = "claude-sonnet"
|
||||
|
||||
[vars]
|
||||
source_env = "production"
|
||||
target_env = "staging"
|
||||
drift_threshold = "warn"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
auto_stop_interval = 120
|
||||
|
||||
[execution.daytona.sandbox.labels]
|
||||
project = "sync-drift"
|
||||
team = "platform"
|
||||
|
||||
[execution.daytona.snapshot]
|
||||
name = "sync-drift-dev"
|
||||
cpu = 2
|
||||
memory = 4
|
||||
disk = 10
|
||||
dockerfile = """
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
||||
git curl ca-certificates jq diffutils \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN useradd -m -s /bin/bash daytona
|
||||
USER daytona
|
||||
WORKDIR /home/daytona
|
||||
"""
|
||||
`,
|
||||
graph: `digraph sync {
|
||||
graph [
|
||||
goal="Detect and resolve drift between product docs, architecture docs, and code",
|
||||
label="Sync"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
detect [label="Detect Drift", prompt="@prompts/sync/detect.md", reasoning_effort="high"]
|
||||
propose [label="Propose Changes", prompt="@prompts/sync/propose.md"]
|
||||
review [shape=hexagon, label="Review Changes"]
|
||||
apply [label="Apply Changes", prompt="@prompts/sync/apply.md"]
|
||||
|
||||
start -> detect
|
||||
detect -> exit [condition="context.drift_found=false", label="No drift"]
|
||||
detect -> propose [condition="context.drift_found=true", label="Drift found"]
|
||||
propose -> review
|
||||
review -> apply [label="[A] Accept"]
|
||||
review -> propose [label="[R] Revise"]
|
||||
apply -> exit
|
||||
}
|
||||
`,
|
||||
},
|
||||
expand: {
|
||||
title: "Expand Product",
|
||||
filename: "expand.dot",
|
||||
description: "Evolves the product by analyzing usage patterns and specifications to propose and implement incremental improvements.",
|
||||
config: `version = 1
|
||||
task = "Propose and implement incremental product improvements"
|
||||
graph = "expand.dot"
|
||||
|
||||
[llm]
|
||||
model = "claude-sonnet"
|
||||
|
||||
[vars]
|
||||
analytics_window = "30d"
|
||||
min_confidence = "0.8"
|
||||
|
||||
[execution]
|
||||
environment = "daytona"
|
||||
|
||||
[execution.daytona.sandbox]
|
||||
auto_stop_interval = 180
|
||||
|
||||
[execution.daytona.sandbox.labels]
|
||||
project = "expand"
|
||||
team = "product"
|
||||
|
||||
[execution.daytona.snapshot]
|
||||
name = "expand-dev"
|
||||
cpu = 2
|
||||
memory = 4
|
||||
disk = 10
|
||||
`,
|
||||
graph: `digraph expand {
|
||||
graph [
|
||||
goal="",
|
||||
label="Expand"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
propose [label="Propose Changes", prompt="@prompts/expand/propose.md", reasoning_effort="high"]
|
||||
approve [shape=hexagon, label="Approve Changes"]
|
||||
execute [label="Execute Changes", prompt="@prompts/expand/execute.md"]
|
||||
|
||||
start -> propose -> approve
|
||||
approve -> execute [label="[A] Accept"]
|
||||
approve -> propose [label="[R] Revise"]
|
||||
execute -> exit
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue