Skip to content
Merged
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
19 changes: 19 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,27 @@ config :vutuv, :ollama_url, "http://10.28.0.3:11434,http://localhost:11434"
# is read with compile_env and is never set in prod/test. Remove this line (or
# set it to false) to restore the strict `default-src 'self'` policy. See the
# long note in VutuvWeb.Plug.ContentSecurityPolicy.
#
# It now carries the Tidewave toolbar too, which is easy to miss: Tidewave adds
# https://tidewave.ai to `script-src` by REWRITING that directive, and only if
# it is already there. Without this flag the policy emits no `script-src` at
# all, `default-src 'self'` governs, and the toolbar script is refused — so
# turning this off costs the toolbar as well as `browser_eval`.
config :vutuv, csp: [allow_eval: true]

# Tidewave's toolbar (injected into every dev HTML page, see the Tidewave plug
# in VutuvWeb.Endpoint) lets you point at an element in the running app and hand
# the agent the HEEx that rendered it. That mapping only exists if LiveView
# annotates its output: `debug_heex_annotations` wraps each component render in
# HTML comments naming its file and line, `debug_attributes` stamps the same
# origin onto the elements themselves. Phoenix 1.8 generates both for new apps;
# vutuv predates that, so they are set here. DEV ONLY — both add markup that has
# no business in a production page, and neither config/prod.exs nor
# config/test.exs sets them.
config :phoenix_live_view,
debug_heex_annotations: true,
debug_attributes: true

config :vutuv, VutuvWeb.Endpoint,
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
url: [host: "localhost", port: String.to_integer(System.get_env("PORT") || "4000")],
Expand Down
8 changes: 7 additions & 1 deletion docs/DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ text + HTML bodies, bounce handling) is described in

### AI tooling in development

[Tidewave](https://tidewave.ai) runs in the dev server (dev-only dependency): AI coding agents can connect to the MCP endpoint at http://localhost:4000/tidewave/mcp to eval code in the running app, query Ecto and read logs.
[Tidewave](https://tidewave.ai) runs in the dev server (dev-only dependency) and offers two ways in.

An **MCP endpoint** at http://localhost:4000/tidewave/mcp, where an AI coding agent evals code in the running app, queries Ecto and reads logs.

A **toolbar** injected into every dev HTML page: point at an element in the browser and hand the agent the HEEx that rendered it. Its Connect icon lets an agent already on the MCP endpoint drive and see that same tab. The source mapping comes from `debug_heex_annotations` and `debug_attributes` in `config/dev.exs`; the toolbar script loads from tidewave.ai, so it needs network access.

Neither reaches production: the dependency is `only: :dev` and `VutuvWeb.Endpoint` plugs Tidewave behind a `Code.ensure_loaded?/1` guard.

### Admin access

Expand Down
18 changes: 15 additions & 3 deletions lib/vutuv_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,21 @@ defmodule VutuvWeb.Endpoint do
)
end

# Tidewave (dev-only dep) exposes an MCP endpoint at /tidewave/mcp so AI
# coding agents can inspect the running app (eval code, query Ecto, read
# logs). The guard keeps the plug out of test and prod builds.
# Tidewave (dev-only dep) does two things from this one plug: it exposes an
# MCP endpoint at /tidewave/mcp so AI coding agents can inspect the running
# app (eval code, query Ecto, read logs), and since v0.8 it injects its
# toolbar into every dev HTML page — dead pages and the LiveView initial
# render alike — so you can point at an element in the browser and hand the
# agent the HEEx behind it (config/dev.exs supplies that mapping).
#
# Position matters: the plug raises if it runs after the request body has
# been parsed or after Phoenix.LiveReloader, so it must stay above the
# `if code_reloading?` block. It does its work from a `before_send` callback,
# which is what lets it rewrite the header VutuvWeb.Plug.ContentSecurityPolicy
# sets much later in the router pipeline (it prepends https://tidewave.ai to
# `script-src` and `connect-src`, and drops `frame-ancestors`).
#
# The guard keeps the plug out of test and prod builds.
if Code.ensure_loaded?(Tidewave) do
plug(Tidewave)
end
Expand Down
8 changes: 4 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Vutuv.MixProject do
def project do
[
app: :vutuv,
version: "7.345.0",
version: "7.345.1",
elixir: "~> 1.20",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand Down Expand Up @@ -40,9 +40,9 @@ defmodule Vutuv.MixProject do
{:phoenix_html_helpers, "~> 1.0"},
{:phoenix_live_view, "~> 1.0"},
{:phoenix_live_reload, "~> 1.4", only: :dev},
# AI-assisted dev tooling: mounts an MCP endpoint at /tidewave/mcp in
# the dev server so coding agents can inspect the running app.
{:tidewave, "~> 0.5", only: :dev},
# AI-assisted dev tooling: an MCP endpoint plus an in-page toolbar for
# coding agents. Plugged (and explained) in VutuvWeb.Endpoint.
{:tidewave, "~> 0.9", only: :dev},
# LiveView's test helpers (`Phoenix.LiveViewTest`) parse the rendered DOM
# with lazy_html; required for the connected-mount assertions.
{:lazy_html, ">= 0.1.0", only: :test},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"tidewave": {:hex, :tidewave, "0.5.6", "91f35540b5599640443f1d3a1c6166bf506e202840261a6344e384e8813c1f64", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "dc82d52b8b6ffc04680544b17cd340c7d4166bb0d63999eb960850526866b533"},
"tidewave": {:hex, :tidewave, "0.9.0", "25f2c0745adba7cb440eed6ec70b900b032ba10d0be884ca2315a2d79ed20085", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.9", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "19259165534283554eda6aafe0d3f00a098ec4c0dca81a1542a89086072c108f"},
"tz": {:hex, :tz, "0.28.2", "6c47f3d1a8ee5c33a1d8f0ba49e5a851b0a30c408a587d907aff6e71228b3b32", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "a6bf7355a33f0a7511602ab4566432ac0901d8abff81e94e455bca19708a0c87"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"},
Expand Down
Loading