Skip to content

Add embeddable API, convo persistence & cancel#14

Merged
jhd3197 merged 35 commits into
mainfrom
dev
May 29, 2026
Merged

Add embeddable API, convo persistence & cancel#14
jhd3197 merged 35 commits into
mainfrom
dev

Conversation

@jhd3197

@jhd3197 jhd3197 commented Mar 1, 2026

Copy link
Copy Markdown
Owner

Introduce an embeddable AgentSite API and conversation persistence so projects can be generated and resumed in-process.

  • Add README docs for the embeddable component and its async API (generate_website, regenerate_page, load_project).
  • Export new types and functions from agentsite and engine packages.
  • Add dataclasses: ConversationMessage, PageState, ProjectState and implement load_project/delete_project in engine.component.
  • Extend GenerationConfig with review/cancellation/context fields and record conversation messages during runs; persist messages to messages.json via ProjectManager.append_message/load_messages.
  • Pipeline enhancements: accept review/cancel/context parameters, emit events (review_feedback, site_plan_ready, style_spec_ready), and short-circuit on cancellation.
  • Add .pr to .gitignore and include a .claude skill for creating PR descriptions.

These changes enable iterative, host-driven workflows (resume, cancel, preview design/review feedback) when embedding AgentSite as a library.

Introduce an embeddable AgentSite API and conversation persistence so projects can be generated and resumed in-process.

- Add README docs for the embeddable component and its async API (generate_website, regenerate_page, load_project).
- Export new types and functions from agentsite and engine packages.
- Add dataclasses: ConversationMessage, PageState, ProjectState and implement load_project/delete_project in engine.component.
- Extend GenerationConfig with review/cancellation/context fields and record conversation messages during runs; persist messages to messages.json via ProjectManager.append_message/load_messages.
- Pipeline enhancements: accept review/cancel/context parameters, emit events (review_feedback, site_plan_ready, style_spec_ready), and short-circuit on cancellation.
- Add .pr to .gitignore and include a .claude skill for creating PR descriptions.

These changes enable iterative, host-driven workflows (resume, cancel, preview design/review feedback) when embedding AgentSite as a library.
Copilot AI review requested due to automatic review settings March 1, 2026 04:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an embeddable (in-process) AgentSite API with disk-backed project/conversation persistence and host-driven controls (review config + cancellation), enabling “generate → resume → iterate” workflows without running the server/UI.

Changes:

  • Add conversation persistence via messages.json and expose project lifecycle helpers (load_project, delete_project) for restoring/resuming work.
  • Extend the generation pipeline/component API with review parameters, conversation context, new events (review_feedback, site_plan_ready, style_spec_ready), and cancellation checks.
  • Update package exports and README documentation to reflect the new embeddable usage.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
agentsite/engine/project_manager.py Adds messages.json persistence helpers (append_message, load_messages) and updates the documented on-disk layout.
agentsite/engine/pipeline.py Adds new pipeline events, review feedback emission, conversation context plumbing, and cancellation short-circuit points.
agentsite/engine/component.py Introduces ConversationMessage/ProjectState dataclasses, load_project/delete_project, and records messages during runs.
agentsite/engine/__init__.py Re-exports new component APIs and state/message types from the engine package.
agentsite/__init__.py Re-exports new embeddable APIs/types at the top-level package.
README.md Adds “Embeddable Component” docs and usage examples for in-process generation and persistence.
.gitignore Ignores /.pr directory used for PR-description tooling artifacts.
.claude/skills/create-pr/SKILL.md Adds a Claude skill doc for generating PR titles/descriptions into .pr/.
Comments suppressed due to low confidence (1)

agentsite/engine/component.py:526

  • If pipeline.generate(...) returns success=False (e.g., cancellation after PM/Designer), this code still persists an assistant message claiming generation succeeded (success: True) and returns a GenerationResult with success=False but no error. Consider branching on result.success (and/or cancel_event.is_set()) to persist an accurate message and set a meaningful error such as "Cancelled by host".
        # Persist assistant message
        pm.append_message(
            project.id,
            ConversationMessage(
                role="assistant",
                content=f"Generated '{slug}' page (v{version}) with {len(files)} files",
                timestamp=datetime.now(timezone.utc).isoformat(),
                meta={"slug": slug, "version": version, "files": files, "success": True},
            ),
        )

        return GenerationResult(
            project_id=project.id,
            slug=slug,
            version=version,
            files=files,
            files_content=files_content,
            output_dir=pm.version_dir(project.id, slug, version),
            usage=getattr(result, "aggregate_usage", {}),
            agent_runs=[r.model_dump() for r in pipeline.agent_runs],
            style_spec=parsed_ss or project.style_spec,
            site_plan_raw=pipeline.site_plan_text,
            success=getattr(result, "success", True),
        )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 599 to +710
@@ -585,6 +625,7 @@ async def _on_round_complete(round_number: int) -> None:
"design_system_guide": design_system_guide,
"architecture_guide": architecture_guide,
"tech_stack": tech_stack.model_dump_json(),
"conversation_context": conversation_context,
}

if "designer" in required_agents:
@@ -622,6 +663,20 @@ async def _on_round_complete(round_number: int) -> None:
style_spec_text = designer_result.shared_state.get("style_spec", "")
self.style_spec_text = style_spec_text

# Emit style_spec_ready so hosts can preview design before dev starts
_style_parsed = False
if style_spec_text:
try:
from prompture import clean_json_text as _cjt
json.loads(_cjt(style_spec_text))
_style_parsed = True
except Exception:
pass
await self._emit("style_spec_ready", data={
"style_spec": style_spec_text,
"parsed": _style_parsed,
})

initial_state["style_spec"] = style_spec_text

# Merge designer usage into pm_result for later aggregation
@@ -642,6 +697,18 @@ async def _on_round_complete(round_number: int) -> None:
else:
initial_state["style_spec"] = StyleSpec().model_dump_json()

# --- Cancellation check after Designer ---
if cancel_event and cancel_event.is_set():
await self._emit("generation_complete", data={
"success": False, "slug": slug, "version": version_number,
"files": [], "error": "Cancelled by host",
})
return GroupResult(
agent_results=[], aggregate_usage={},
shared_state={}, elapsed_ms=0, timeline=[], errors=[],
success=False,
)

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancellation is only checked after the PM and Designer phases. If cancel_event is set while the build/review pipeline is running, generation will continue to completion because the event isn't propagated into Prompture groups. If the intent is host-driven cancellation, consider wiring cancellation into the agent/group execution (if Prompture supports it) or adding additional checks between major steps so cancel requests take effect promptly.

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +190 to +213
from agentsite import generate_website, regenerate_page, GenerationConfig

# Generate a site from a prompt
result = await generate_website(
"A dark portfolio site with projects and contact page",
output_dir=Path("./websites"),
config=GenerationConfig(
model="openai/gpt-4o",
provider_keys={"openai": os.environ["OPENAI_API_KEY"]},
max_cost=0.50,
),
on_event=lambda e: print(f"{e.agent}: {e.type}"),
)

for path, html in result.files_content.items():
print(f"{path}: {len(html)} bytes")

# Iterate on the same project with new feedback
v2 = await regenerate_page(
"Make the hero section taller and add a testimonials page",
output_dir=Path("./websites"),
project_id=result.project_id,
config=GenerationConfig(model="openai/gpt-4o"),
)

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embeddable API example uses Path and os.environ but doesn't import Path or os, and uses await at top-level (which will error outside an async context). Adding the missing imports and indicating that this runs inside async def (or showing asyncio.run(...)) would make the snippet copy/pasteable.

Suggested change
from agentsite import generate_website, regenerate_page, GenerationConfig
# Generate a site from a prompt
result = await generate_website(
"A dark portfolio site with projects and contact page",
output_dir=Path("./websites"),
config=GenerationConfig(
model="openai/gpt-4o",
provider_keys={"openai": os.environ["OPENAI_API_KEY"]},
max_cost=0.50,
),
on_event=lambda e: print(f"{e.agent}: {e.type}"),
)
for path, html in result.files_content.items():
print(f"{path}: {len(html)} bytes")
# Iterate on the same project with new feedback
v2 = await regenerate_page(
"Make the hero section taller and add a testimonials page",
output_dir=Path("./websites"),
project_id=result.project_id,
config=GenerationConfig(model="openai/gpt-4o"),
)
import os
import asyncio
from pathlib import Path
from agentsite import generate_website, regenerate_page, GenerationConfig
async def main() -> None:
# Generate a site from a prompt
result = await generate_website(
"A dark portfolio site with projects and contact page",
output_dir=Path("./websites"),
config=GenerationConfig(
model="openai/gpt-4o",
provider_keys={"openai": os.environ["OPENAI_API_KEY"]},
max_cost=0.50,
),
on_event=lambda e: print(f"{e.agent}: {e.type}"),
)
for path, html in result.files_content.items():
print(f"{path}: {len(html)} bytes")
# Iterate on the same project with new feedback
v2 = await regenerate_page(
"Make the hero section taller and add a testimonials page",
output_dir=Path("./websites"),
project_id=result.project_id,
config=GenerationConfig(model="openai/gpt-4o"),
)
if __name__ == "__main__":
asyncio.run(main())

Copilot uses AI. Check for mistakes.
Comment thread README.md
| `agent_configs` | `dict[str, AgentConfig] \| None` | `None` | Per-agent overrides |
| `style_spec` | `StyleSpec \| None` | `None` | Pre-defined design tokens |
| `logo_url` | `str` | `""` | Logo URL for the site |
| `icon_url` | `str` | `""` | Favicon URL |

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenerationConfig gained new fields (max_review_iterations, review_threshold, cancel_event, conversation_context) but the README table doesn't list them. Please document these fields (and defaults/behavior) so embedders know how to enable review gating and cancellation/context features.

Suggested change
| `icon_url` | `str` | `""` | Favicon URL |
| `icon_url` | `str` | `""` | Favicon URL |
| `max_review_iterations` | `int` | `0` | Maximum number of automated review/fix cycles per page. `0` disables review gating and accepts the first draft. |
| `review_threshold` | `float` | `0.0` | Minimum review score (0.0–1.0) required to accept a page when review gating is enabled. Only used when `max_review_iterations > 0`. |
| `cancel_event` | `Any \| None` | `None` | Optional cooperative-cancellation flag (e.g. a `threading.Event`). When set during generation, the run aborts as soon as possible. |
| `conversation_context` | `dict[str, Any] \| None` | `None` | Extra context injected into all agent prompts (e.g. user/session/site metadata). Must be JSON-serializable. |

Copilot uses AI. Check for mistakes.
Comment thread agentsite/engine/project_manager.py Outdated
Comment on lines +191 to +207
def append_message(self, project_id: str, message: object) -> None:
"""Append a ConversationMessage (dataclass) to messages.json."""
import dataclasses

path = self._messages_path(project_id)
messages: list[dict[str, Any]] = []
if path.exists():
messages = json.loads(path.read_text(encoding="utf-8"))
messages.append(dataclasses.asdict(message))
path.write_text(json.dumps(messages, indent=2), encoding="utf-8")

def load_messages(self, project_id: str) -> list[dict[str, Any]]:
"""Load conversation messages from disk. Returns [] if file is missing."""
path = self._messages_path(project_id)
if not path.exists():
return []
return json.loads(path.read_text(encoding="utf-8"))

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_message/load_messages call json.loads on messages.json without handling JSONDecodeError or validating the parsed type. A partially-written/corrupted file (or manual edits) will crash generation/load. Consider catching decode errors, defaulting to [], and (optionally) validating that the loaded value is a list of dicts before appending/returning.

Suggested change
def append_message(self, project_id: str, message: object) -> None:
"""Append a ConversationMessage (dataclass) to messages.json."""
import dataclasses
path = self._messages_path(project_id)
messages: list[dict[str, Any]] = []
if path.exists():
messages = json.loads(path.read_text(encoding="utf-8"))
messages.append(dataclasses.asdict(message))
path.write_text(json.dumps(messages, indent=2), encoding="utf-8")
def load_messages(self, project_id: str) -> list[dict[str, Any]]:
"""Load conversation messages from disk. Returns [] if file is missing."""
path = self._messages_path(project_id)
if not path.exists():
return []
return json.loads(path.read_text(encoding="utf-8"))
def _load_messages_file(self, path: Path) -> list[dict[str, Any]]:
"""Safely load messages from a JSON file, returning [] on errors."""
if not path.exists():
return []
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
# Corrupted, partially written, or unreadable file: treat as empty.
return []
if not isinstance(raw, list):
return []
# Ensure we only return list elements that are dict-like.
messages: list[dict[str, Any]] = []
for item in raw:
if isinstance(item, dict):
messages.append(item)
return messages
def append_message(self, project_id: str, message: object) -> None:
"""Append a ConversationMessage (dataclass) to messages.json."""
import dataclasses
path = self._messages_path(project_id)
messages: list[dict[str, Any]] = self._load_messages_file(path)
messages.append(dataclasses.asdict(message))
path.write_text(json.dumps(messages, indent=2), encoding="utf-8")
def load_messages(self, project_id: str) -> list[dict[str, Any]]:
"""Load conversation messages from disk. Returns [] if file is missing."""
path = self._messages_path(project_id)
return self._load_messages_file(path)

Copilot uses AI. Check for mistakes.
Comment thread agentsite/engine/project_manager.py Outdated
Comment on lines +191 to +200
def append_message(self, project_id: str, message: object) -> None:
"""Append a ConversationMessage (dataclass) to messages.json."""
import dataclasses

path = self._messages_path(project_id)
messages: list[dict[str, Any]] = []
if path.exists():
messages = json.loads(path.read_text(encoding="utf-8"))
messages.append(dataclasses.asdict(message))
path.write_text(json.dumps(messages, indent=2), encoding="utf-8")

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_message uses a read-modify-write cycle on messages.json with no synchronization, so concurrent generations on the same project_id can lose messages or write invalid JSON. If concurrent use is expected for the embeddable API, consider a file lock, atomic write via temp+rename, or switching to an append-friendly format (e.g., JSONL).

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 26
"""Manages project directories and files on disk.

Filesystem layout::

{base}/{project_id}/
├── project.json
├── messages.json
├── assets/
├── guides/
└── pages/

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filesystem layout docstring now lists guides/ and messages.json, but create() only creates pages/ and assets/. Either create those paths during create() or adjust the documented layout so it matches what a newly-created project directory actually contains.

Copilot uses AI. Check for mistakes.
Comment thread agentsite/engine/project_manager.py Outdated
Comment on lines +203 to +208
"""Load conversation messages from disk. Returns [] if file is missing."""
path = self._messages_path(project_id)
if not path.exists():
return []
return json.loads(path.read_text(encoding="utf-8"))

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New message persistence behavior (append_message/load_messages) is not covered by tests. Since ProjectManager already has a dedicated test module, adding tests for create→append→load (and corrupt/empty file handling) would help prevent regressions.

Suggested change
"""Load conversation messages from disk. Returns [] if file is missing."""
path = self._messages_path(project_id)
if not path.exists():
return []
return json.loads(path.read_text(encoding="utf-8"))
"""Load conversation messages from disk. Returns [] if file is missing or invalid."""
path = self._messages_path(project_id)
if not path.exists():
return []
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
# Corrupt or empty JSON; treat as no messages
return []
if not isinstance(data, list):
# Unexpected structure; be defensive and return no messages
return []
return data

Copilot uses AI. Check for mistakes.
jhd3197 and others added 26 commits March 1, 2026 00:08
Introduce cooperative cancellation to the generation pipeline, and harden ProjectManager message persistence with safe reads and atomic writes. Changes:

- pipeline: check cancel_event after build/review, emit generation_complete with error and return a failed GroupResult when cancelled.
- project_manager: create a guides/ directory for new projects; add _safe_read_messages to tolerate missing/corrupt/non-list JSON and filter non-dict items; make append_message use atomic write (temp file + replace) to avoid corruption; load_messages now uses the safe reader.
- tests: add coverage for guides directory, message append/load, and multiple edge cases (nonexistent, corrupted JSON, non-list, filtering non-dict items).
- README: replace top-level await snippet with an asyncio.run example and add new API docs for max_review_iterations, review_threshold, cancel_event, and conversation_context; small import fixes.

These changes improve reliability for concurrent/interrupted writes, provide a cooperative cancellation mechanism, and update docs/tests to reflect the new behaviors.
- Add DiscoveryBrief model + form schema ported from open-design
- New GET /api/discovery/form endpoint
- Wire discovery_brief through GenerateRequest -> pipeline -> PM/Designer prompts
- Emit discovery_brief_submitted WS event
- DiscoveryForm.jsx renders before first generation in PageBuilderPage
- PM persona updated to treat discovery brief as authoritative
- tests/test_phase1_discovery.py (10 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add agentsite/agents/directions.py — 5 directions ported verbatim from
  open-design (editorial-monocle, modern-minimal, human-approachable,
  tech-utility, brutalist-experimental) with OKLch palettes
- StyleSpec gains direction_id + parallel OKLch fields (back-compat)
- New /api/directions{,/:id} endpoints
- GenerateRequest accepts direction_id; pipeline synthesizes StyleSpec
  deterministically when brand_mode=='pick_direction' and direction_id set,
  skipping the Designer agent entirely
- DirectionPicker.jsx renders after DiscoveryForm when user chose
  pick_direction without supplying a direction
- tests/test_phase2_directions.py (12 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- write_file checks ctx.deps['_preflight_required'] and returns an
  actionable error until every required guide has been attempted via
  read_guide; self-disarms after the first satisfied write
- read_guide records the attempt regardless of whether the file exists,
  so cold-start runs (no guides yet) still pass after asking
- pipeline.py initializes the gate per page-build from settings
- New settings: preflight_enabled (default True) +
  preflight_required_guides (default design-system.md, architecture.md)
- tests/test_phase3_preflight.py (6 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New models: DimensionScore, ReviewVerdict, QualityRatchet, CRITIQUE_DIMENSIONS
- agentsite/agents/critique.py: 4 single-dim reviewers (visual_fidelity,
  accessibility, content_quality, code_health) + judge that aggregates to
  ReviewVerdict JSON via Prompture's AsyncDebateGroup (rounds=1)
- agentsite/engine/ratchet.py: load/update_ratchet, only-up rule,
  per-project quality_ratchet.json with append-only history
- GET /api/projects/{id}/quality returns the ratchet
- Pipeline runs the panel + updates ratchet after successful generation
  when settings.use_critique_panel is True (default OFF until Phase 11
  offsets the extra cost). Emits critique_verdict WS event
- tests/test_phase4_critique.py (13 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agentsite/skills/ directory with 8 bundled skills:
  saas-landing, pricing-page, dashboard, docs-page, blog-post,
  portfolio, mobile-app, coming-soon. Each is a SKILL.md with YAML
  frontmatter (mode, platform, scenario, default_for, example_prompt)
  loaded via prompture.load_skill_from_directory
- agentsite/skills/__init__.py: discover_bundled_skills, find_skill,
  skill_summary helpers
- GET /api/skills + GET /api/skills/{name} endpoints
- PagePlan gains optional skill_id
- PM persona lists available skills and is told to set page.skill_id
- Pipeline resolves the per-page skill, injects instructions into shared
  state as {skill_instructions} + emits skill_bound WS event
- Developer / markup prompts pull {skill_instructions} into their context
- tests/test_phase5_skills.py (10 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- write_file fires on_preview_update(path, content) for *.html writes
- pipeline.py bridges the callback to a preview_update WS event with a
  short sha1 content_hash for iframe key=hash remount semantics
- PreviewFrame accepts {html, contentHash} and switches to srcdoc when
  html is present; falls back to src= once generation_complete clears
  the live state
- useGeneration tracks livePreview per page slug; clears on completion
- PageBuilderPage passes livePreview[slug] into PreviewFrame
- tests/test_phase6_srcdoc.py (3 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agentsite/engine/interrupt.py: SteerMailbox singleton with
  per-project asyncio.Queue, non-blocking deposit + destructive drain
- WS endpoint handles inbound {type: "steer", text: "..."} frames, posts
  to the mailbox, broadcasts steer_received
- Pipeline drains the mailbox just before the build phase and injects
  the accumulated tweaks into developer prompts via {user_steer}; emits
  steer_applied with the rendered text + count
- New agentsite/agents/developer.py::create_developer_deep_agent —
  AsyncDeepAgent with planning on (VFS/sub-agents/summarization OFF so
  the on-disk write_file tool stays load-bearing). Gracefully falls
  back to the tool-calling developer when the installed prompture
  predates AsyncDeepAgent
- Settings flag use_deep_agent_developer (default off)
- WSEvent types listed: todo_update, steer_received, steer_applied
- tests/test_phase7_interrupt.py (7 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agents/brand_extractor.py:
  - extract_from_url: httpx fetch of HTML + linked .css, sanitize via
    PromptInjectionDetector + PIIRedactor when available, regex hexes,
    sniff fonts from known Google + system family list, classify
    bg/surface/text/accent slots by luminance + chroma
  - extract_from_image: Pillow quantization to 8 dominant colors,
    same classification pipeline (graceful no-op when Pillow missing)
  - extract_from_pdf: prefer prompture.ingestion when present,
    fall back to raw bytes regex
- New api/deps.py::guard_external_url — SSRF guard rejecting:
  non-http(s) schemes, missing hostnames, DNS failures, and any
  resolved IP that is private / loopback / link-local / reserved
- New POST /api/projects/{id}/brand/extract/{url,image,pdf}
  endpoints with size caps and optional persist-to-project
- tests/test_phase8_brand.py (14 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agentsite/design_systems/ with 4 starter systems:
  linear, vercel, stripe, notion. Each has DESIGN.md (voice + posture)
  + tokens.css (CSS custom properties)
- discover_design_systems() loads them; _parse_tokens_css extracts
  the :root variables; summary() includes a 6-swatch palette preview
- StyleSpec.inherits_from: optional design-system id (back-compat)
- GenerateRequest accepts top-level inherits_from; pipeline reads
  project.style_spec.inherits_from and prepends an "inherit from this
  system, don't replace it" block (with the system's raw_css) to the
  Designer prompt
- New /api/design-systems endpoints: list, detail, save-user-system
- tests/test_phase9_design_systems.py (11 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New project_memories table (additive migration); MemoryFact model
- MemoryRepository: create/list_by_project/delete/delete_by_project
- New engine/memory.py: extract_memories heuristic over
  DiscoveryBrief + steer lines + ReviewVerdict (no LLM call). Dedup
  + classification (preference / constraint / brand / other).
  render_for_context formats memories for prompt injection
- Pipeline loads top 15 facts at run start and prepends them to the PM
  prompt alongside the DiscoveryBrief; extracts new facts after
  successful generation, dedupes against existing, persists,
  and emits memory_extracted WS event
- New CRUD endpoints under /api/projects/{id}/memories
- tests/test_phase10_memory.py (10 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Settings.agent_routing maps agent_key -> strategy ("fast",
  "cost_optimized", "balanced", "quality_first") OR explicit model id
- Settings.routing_model_pools holds the candidate pool per strategy
  (empty by default; falls back to global default_model)
- New model_resolver.route_for: explicit config wins, then
  strategy/explicit hint, then standard fallback chain. Default
  routing biases the critique panel reviewers toward cheap models
  and the judge toward quality_first — pairs with Phase 4 to offset
  the panel's extra cost
- New engine/rag_index.py: lightweight token-overlap retrieval over
  bundled skills + design systems, process-cached, invalidate()-able.
  Backend-agnostic surface — Chroma upgrade lands in a follow-up
- tests/test_phase11_routing.py (11 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- agentsite/engine/refusal.py: detect_refusal heuristic over a known
  set of refusal phrases; prefers prompture.refusal.RefusalDetector
  when available. pick_fallback_model helper for retry sites
- Pipeline emits refusal_detected WS event when any agent's output
  text matches the heuristic (non-fatal — existing fallback chain
  still handles empty/garbled output)
- agentsite/prompt_templates/web/: 6 starter templates
  (saas-landing, portfolio, docs, dashboard, pricing, coming-soon)
  each linking a skill_id + direction_id + concrete prompt
- New GET /api/prompt-templates endpoint
- agentsite/THIRD_PARTY_NOTICES.md attributing Open Design (Apache-2.0)
  for the porting work across Phases 1, 2, 4, 5, 7, 8
- tests/test_phase12_polish.py (9 cases, all pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The _migrate() ALTER TABLE only runs when the column is missing on an
existing table; on a brand-new DB the PRAGMA returns no columns at all,
the ALTER branch short-circuits, and executescript(SCHEMA_SQL) then
creates the table without user_id. Add the column to SCHEMA_SQL so
fresh DBs match migrated DBs.

Unblocks 13 pre-existing test_storage + test_api failures.
Full suite: 203 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- QualityRatchetChart aggregates per-project ratchets, showing the
  current floor per dimension and accepted/total run count. Reads
  /api/projects then /api/projects/{id}/quality for the top 5
  projects with any ratchet history
- RefusalRateChart scans the AgentRun output_summary blob for the
  Phase 12 refusal marker, shows rate + per-agent breakdown
- Pipeline now persists the refusal info into the active AgentRun's
  output_summary (in addition to emitting the WS event) so analytics
  can query historical refusals, not just real-time ones
- Wired both into AnalyticsPage; Charts row gets a 3rd column,
  new Quality row below for the per-project ratchet cards

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r, templates)

- BrandExtractor (Phase 8): 3-tab uploader (URL / image / PDF),
  calls /api/projects/{id}/brand/extract/* with persist flag and a
  swatch preview of the result
- MemoryPanel (Phase 10): list + add + delete project memories with
  kind chips (preference / constraint / brand / other) and a
  confidence percentage
- DesignSystemPicker (Phase 9): grid of bundled + user systems with
  6-swatch previews; clicking PUTs style_spec.inherits_from to the
  project
- TemplateGallery (Phase 12): cards on DashboardPage that prefill
  the create-project modal with the template's name + prompt
- All three new panels mounted in ProjectBrandPage between the
  existing BrandContent and KnowledgeBase sections

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Pipeline calls rag_index.retrieve(page_prompt) twice — once for
  skills (k=5) and once for design systems (k=3) — and inlines the
  ranked top hits into the PM prompt as new "Available skills" and
  "Candidate design systems" blocks
- PM persona's hardcoded skill list removed in favor of "pick from the
  ranked Available skills list in the user message". This keeps the
  PM's context proportional to the current brief instead of growing
  with the full catalog
- Catalog growth is now a data-only concern: dropping in a new
  agentsite/skills/<id>/SKILL.md or design_systems/<id>/ entry
  surfaces automatically when relevant

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strategy column:
- agent_runs gains strategy + model columns (additive migration);
  AgentRun model gains both fields
- Pipeline stamps the active run with the routing strategy that
  picked its model and the concrete model id at agent-start
- New CostByRoutingChart aggregates per-strategy spend from runs,
  mounted alongside QualityRatchet in the Analytics page

SQLite-backed design systems:
- New design_systems table; DesignSystemRepository with CRUD
- /api/design-systems/{list,get,save,delete} now persist via SQLite
  instead of an in-process dict (survives restarts)
- Saving an id that collides with a bundled system returns 409
- Deleting a bundled system returns 400; user systems delete cleanly
- Phase 9 tests updated to use TestClient context manager so the
  FastAPI lifespan initializes the DB

Full suite: 205 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TodoStream component renders the live plan with status-aware icons
  (pending / in_progress with spinner / completed checkmark)
- ChatInput grows a Steer mode whenever generating: pressing Enter
  routes the message through ws.send({type:"steer", text}) instead
  of starting a new generation. Button label + color flip too
- useGeneration exposes a steer(text) helper and tracks the todos
  array from the todo_update WS event (resets on each start /
  completion)
- ChatSidebar passes both downward; PageBuilderPage mounts the
  TodoStream while generating
- Pipeline emits todo_update after the developer agent completes,
  reading the deep_state.todos snapshot. Silent no-op when the
  developer isn't a DeepAgent (use_deep_agent_developer=False or
  AsyncDeepAgent unavailable in installed prompture)

Full suite: 205 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- frontend/public/frames/{iphone-15-pro,android-pixel,ipad-pro,macbook}.svg
  authored as minimal device chrome (titanium edges, Dynamic Island,
  punch-hole, notch + chin, etc) in their native viewBox pixel space
- New DeviceFrame.jsx wraps a child iframe inside the SVG's screen
  safe-area rectangle using percentage positioning so it scales
- DeviceSwitcher gets 4 new frame buttons (Apple, Android, iPad, MacBook
  icons) alongside the existing Desktop/Tablet/Mobile width buttons.
  onChange signature extended to (width, frame) — backwards-compat
- PreviewFrame: when `frame` is passed, drops the synthetic browser
  chrome and wraps the iframe in DeviceFrame instead. Both srcdoc
  (Phase 6 live preview) and src= modes work inside the device chrome
- PageBuilderPage tracks deviceFrame alongside device width

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- pyproject: [tool.setuptools.package-data] globs ship the SKILL.md
  bodies, design-system DESIGN.md + tokens.css, prompt-template JSON,
  and THIRD_PARTY_NOTICES.md inside a built wheel. Without these the
  loaders (skills/__init__.py, design_systems/__init__.py,
  prompt_templates/__init__.py) silently return empty in a wheel-
  installed environment
- README: 11 new Features subsections covering Phases 1-12 work
  (discovery brief, direction picker, skill catalog + RAG, design
  system inheritance, critique panel + ratchet, pre-flight, steer
  mailbox, srcdoc live preview, brand extraction, memory, smart
  routing, refusal detection, device frames, template gallery)
- README: new "Phase 1-12 Feature Flags" reference table covering
  every settings.py knob + per-project + per-generation override

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single doorway covering: what shipped (20 commits since f5b0979),
quickstart, every feature flag, every WSEvent type and its UI handler,
the manual golden-flow checklist, known follow-up tasks, the file
read-order for picking up the codebase cold, and the migration /
test-client / Prompture-version gotchas that bit me along the way.

Suite: 205 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the inline _extract_fenced_blocks regex loop and
_try_extract_raw_html regex loop in pipeline.py with calls to
prompture's extract_fenced_blocks / extract_html_document utilities
(shipped in prompture 1.1.3).  The pipeline's wrappers now only
contain the AgentSite-specific filename routing + write-file plumbing.

Also fix brand_extractor for prompture 1.1.x:
- PIIRedactor().redact() now returns a RedactionResult dataclass, not
  a string; pull the redacted text off result.text.
- Hex extraction must run on the raw text, not sanitised: the new
  redactor flags '#111111' as a phone number and rewrites it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each factory now builds a prompture.Assistant (persona + tools + model
+ capability hints) and returns assistant.build_async_agent() for the
pipeline to drive.  No runtime changes — the pipeline still consumes
the AsyncAgent / AsyncDeepAgent the Assistant constructs.

Behavioural parity:
- Tool-calling Developer (full LLM + write_file tools)
- Deep Developer (write_todos + VFS off + summarisation off)
- Plain Developer (no tools; markdown-fenced HTML response)
- Reviewer in full / tools-only / plain variants, with JSON-schema
  reinforcement appended to the persona when structured output isn't
  available.

The DEVELOPER_PERSONA and REVIEWER_PERSONA constants are unchanged;
the plain-mode persona moved into developer.py as a Persona instance
instead of a raw string, so all factories share the same construction
shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a chat-driven build flow and extract reusable generation logic.

- Add agents/chat_tools.py: tool registry for the chat agent (start_build, steer_build, page/version inspectors).
- Add api/routes/chat.py: streaming SSE chat endpoint backed by prompture.AsyncAgent that can call tools and persists messages.
- Add engine/generation_runner.py: centralized start_generation_task to factor pipeline setup and background task wiring (used by both REST generate and chat).
- Update api/app.py to include the chat router and refactor generate route to call start_generation_task (with proper HTTP error mapping).
- Update agents/tools.py to use prompture image generation driver and base64 decode image data; require OPENAI_API_KEY check remains.
- Add frontend/src/api/chat.js: client helper to stream chat SSE frames and deliver parsed events to callers.
- Update frontend hook useGeneration.js: add prepareBuildStream to reset state and open WS for builds (used by direct starts and chat-initiated builds).
- Remove legacy UI components frontend/src/components/builder/ProgressPipeline.jsx and TodoStream.jsx (deleted).

The change centralizes generation startup logic so multiple callers (REST and chat agent) share the same behavior, enables live streaming chat with tool invocation, and wires up front-end support for consuming SSE chat events.
…ition

1. chat_tools.start_build now accepts and forwards discovery_brief
   (audience, visual_tone, direction_id, constraints).  Previously the
   chat-initiated builds always ran with brief=None, leaving the model
   to guess brand context.

2. Chat persona instructs the agent to infer audience + visual_tone
   from the user's prompt before calling start_build, with examples
   for tech-utility / editorial / luxury surfaces.  Empty values
   reliably produced off-brand output.

3. Pipeline guard: when PM omits 'designer' from required_agents AND
   no direction was picked AND no inherited style exists, reinstate
   'designer'.  Weaker PM models (kimi-k2 was the trigger) were
   skipping the designer on fresh page builds, which left the
   developer agent free-styling the brand identity.

4. ChatSidebar renders the discovery form at the top (sticky), scrolls
   it into view when it appears, and hides the empty-state hero while
   it's showing.  The previous layout rendered the form BELOW the
   message list, and the auto-scroll-to-bottom effect frequently
   buried it off-screen — users could miss the brief entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jhd3197 added 8 commits May 25, 2026 18:42
Add server and frontend support for persisting visual edits made in the PreviewFrame.

- New FastAPI route /api/edit that writes edited HTML into a version directory, prevents path traversal, and mirrors changes into the version repository.
- Register the new edit router in agentsite.api.app.
- Add frontend vendor/htmlstudio and multiple frontend pieces (edit client, EditInspector, useVisualEdit hook, ThemeToggle/ThemeContext, sync script) to drive the visual editor UI.
- Update Dockerfile to copy vendor/manifests before npm install and avoid using host package-lock to prevent platform-specific optional-deps issues; include vendor in container build cache strategy.
- Update .dockerignore and .gitignore to ignore frontend build artifacts and node_modules, and update frontend package.json/package-lock.json to reference the local htmlstudio vendor and refresh lockfile.

These changes enable editing pages in-browser and persisting the final HTML back to the project's versioned files.
When the visual editor is open with an element selected, the chat
persona switches from "build a site" to "tweak this element". The agent
gets the selection (id, tag, kind, current text/style/href/src) in its
prompt and is restricted to a single tool — `patch` — that mirrors
htmlstudio's Patch union. The frontend intercepts `tool_use_stop` for
patch and routes the payload through `visualEdit.applyPatch`, so agent
edits and inspector edits share one rail.

- chat_tools.py: new `patch` tool + `edit_registry` (patch + read_page_file)
- chat.py: `EditContext` model, `EDIT_MODE_SYSTEM_PROMPT`, branches
  registry + system prompt + framed prompt on `edit_context.mode`
- chat.js: `streamChat` accepts `editContext` opt
- PageBuilderPage: passes editContext; intercepts patch tool calls and
  routes to visualEdit.applyPatch
- ChatSidebar: top banner showing edit-mode + selected element tag/id

Plan reference: .planning/htmlstudio-roadmap.md §Phase 1
Backend (new chat tools, edit_registry only):
- find(selector, limit) — CSS-select elements with data-ve-id
- get_tree(id, depth) — nested structural view
- find_closest(from_id, selector) — walk up to nearest matching ancestor
- get_parent(id) / get_children(id) — immediate relatives
Backed by agentsite/engine/html_query.py — a thin BeautifulSoup port of
htmlstudio's query module. Patch application stays client-side; this is
read-only discovery for the edit-mode agent.

Edit-mode system prompt now teaches the agent the BULK pattern: call
find first, then loop patches; resolve "this section" / "the parent"
with find_closest / get_parent before patching.

Frontend:
- useVisualEdit gains `selections[]`, `applyPatches`, `clearSelection`,
  and a previewFrameRef (host → iframe commands)
- EditInspector switches to a multi-mode header + "Bulk edit" hint when
  selections.length > 1; Typography / Layout / Appearance fan-out one
  patch per id via applyPatches; Content / Advanced sections hide.
- ChatSidebar banner shows "N elements selected" in purple for multi
- editContext sent over the chat stream now carries selections[]

htmlstudio v0.2.0 bumped + vendored:
- src/query.ts API (findById, findAll, findClosest, getChildren,
  getParent, getTree) — 14 new tests
- bridge: shift-click toggles multi-select (select-multi event),
  Escape clears, host can POST query → query-result
- ElementInfo gains .block (data-ve-block) — Phase 3 groundwork

.gitignore: whitelist frontend/vendor/htmlstudio/dist/ so vendored
prebuilt JS is committed (was hidden by global dist/ rule, breaking
fresh clones and Docker reproducibility).

Plan reference: .planning/htmlstudio-roadmap.md §Phase 2
Ships 4 reusable blocks (hero-split, cta-banner, feature-grid-3,
testimonial-quote) with declared editable fields and a base64-encoded
JSON config attached to each instance via data-ve-config.

Insert flow:
  1. User clicks "Block" in edit mode → BlockPalette opens
  2. Pick a block + click → renders via htmlstudio renderBlock()
  3. set-outer-html patch replaces the selected element

Re-render flow:
  1. Click an inserted block → EditInspector shows a typed BlockConfigForm
     instead of generic CSS sections
  2. Edit fields + Apply → renderBlockUpdate() with the same instanceId
  3. set-outer-html patch replaces the instance

Agent flow (new edit_registry tools):
  - list_blocks → discover the catalog
  - render_block(block_id, config) → returns HTML string
  - patch(kind='set-outer-html', id=<target>, html=<rendered>)
The system prompt now teaches the find→render→patch pattern.

Backend:
- agentsite/engine/blocks.py: Python mirror of BUILTIN_BLOCKS +
  render_block(). Mirrored manually (small enough — 4 blocks) so the
  agent can render server-side without a Node subprocess.

Frontend:
- frontend/src/api/blocks.js: thin wrapper around htmlstudio's exports
- BlockPalette: searchable, category-filtered modal
- BlockConfigForm: renders typed inputs (text/textarea/color/url/image/
  number/boolean/select) from BlockField schema
- EditInspector: branches to BlockConfigForm when selection.block is set;
  decodes the base64 config attribute to populate the form
- PageBuilderPage: "+ Block" button in edit mode, palette modal,
  onRerenderBlock handler wired through the existing applyPatch pipeline

htmlstudio v0.3.0:
- src/blocks.ts: BlockDefinition + render/read/update helpers
- BUILTIN_BLOCKS + BUILTIN_REGISTRY exports
- 14 new tests; 58/58 passing

Deferred to v0.4 per the roadmap: data sources (RSS/JSON/REST hydration).

Plan reference: .planning/htmlstudio-roadmap.md §Phase 3
ChatSidebar: edit-mode banner now visually distinct — gradient brand→
purple background, larger header with shadowed icon, explicit "no
rebuild" tag, plus a one-line explanation of what the agent can
actually do (patch / find / insert blocks) and what it CANNOT do
(rerun the PM pipeline). Selected element line stays at the bottom.

.planning/htmlstudio-roadmap.md: append Phase 4 — project component
library ("save selection as component"). Spec includes:
  - project_components SQLite table + repo
  - component_extractor.py heuristics (auto-detect editable fields
    from selection structure)
  - REST routes + SaveComponentModal + library tab in BlockPalette
  - extract_component / list_project_components chat tools
  - Edge cases: slug conflicts, empty extractions, XSS surface,
    propagation policy (recommended: opt-in via confirm)
  - 7 acceptance criteria

.planning/agent-pipeline-redesign.md: new brainstorm doc. Captures
the "PM → Designer → Developer is too rigid + need specialists"
thread without committing to an implementation:
  - 4 design directions (right-size triage / orchestrator above PM /
    peer-call agents / vertical skills)
  - Specialist agent menu (Photo, Copywriter, SEO, A11y, Motion,
    Brand, Performance, Data, Localization)
  - What's already true (edit-mode IS the lightweight path)
  - 5 open questions to resolve before any plan
  - Sequenced "start with this" path: instrument → triage classifier
    → pull Photo specialist out → decide → eval harness

Both docs are self-contained for fresh-session execution.
Closes the GrapesJS-shaped gap where custom blocks meant writing code.
Any selected element can now be saved as a reusable BlockDefinition
scoped to its project, then re-inserted via the same palette/render
pipeline that already serves the 4 built-in blocks.

Backend:
- project_components SQLite table + ProjectComponentRepository
- ProjectComponent / BlockFieldModel pydantic models
- agentsite/engine/component_extractor.py — walks selected HTML,
  proposes one BlockField per leaf (h1/h2/h3 → headings, p → bodies,
  a → cta_text+cta_href, img → image+alt), detects repeating colors
  as accent fields. HTML mutated in-place so the resulting template
  carries {{key}} placeholders ready for renderBlock().
- agentsite/api/routes/components.py — full CRUD + render endpoints
  - GET    /api/projects/{id}/components
  - POST   /api/projects/{id}/components       (extract + persist)
  - GET    /api/projects/{id}/components/{cid}
  - PUT    /api/projects/{id}/components/{cid} (commit refinements)
  - DELETE /api/projects/{id}/components/{cid}
  - POST   /api/projects/{id}/components/{cid}/render
- Wired into deps + app lifespan
- Two new edit-mode chat tools:
  - list_project_components — discover the library
  - extract_component(target_id, name, slug) — save a section
- render_block falls back to project components when block_id is a slug
- EDIT_MODE_SYSTEM_PROMPT now teaches both INSERTING (check both
  catalogs) and EXTRACTING (ask first when proposing on repetition)

Frontend:
- frontend/src/api/components.js — fetch wrappers
- SaveComponentModal — two-stage flow (meta → refine fields), uses
  the new visualEdit.getOuterHtml(id) to read the selection's markup
- BlockPalette — tabs: "Built-in (4)" vs "This project (N)"; empty
  state hints at the Save flow when the project lib is empty
- EditInspector — "Save as" button in header for non-block single-
  select; existing block-config form stays for already-inserted blocks
- useVisualEdit — new getOuterHtml(id) reads from tagged source via
  DOMParser; returned alongside selection/applyPatch
- PageBuilderPage — fetches projectComponents on mount + after save,
  threads them into BlockPalette; project components insert via
  renderComponent REST call (server-side render preserves the slug as
  the block id stamped on the instance)

v0.4 propagation policy: instances are FROZEN at insert time. Editing
a component's definition doesn't retroactively update existing
instances — they keep the HTML they were stamped with. This is
deliberately conservative; opt-in propagation is on the v0.5 docket
per .planning/htmlstudio-roadmap.md §4.6.

Plan reference: .planning/htmlstudio-roadmap.md §Phase 4
                                  + Create-new-design button

Big sidebar restructure. Four pieces ship together:

1. <ChatPanel> + useChat + <StandaloneChat>
   - frontend/src/components/chat/ChatPanel.jsx — presentational chat
     surface, takes everything via props (messages, onSend, topBanner,
     stickyForm, belowMessages, emptyState slots). Framework-aware-ish
     but no streaming state of its own.
   - frontend/src/hooks/useChat.js — owns chat state + Prompture SSE
     streaming. Config: projectId/slug/model/editContext/toolHandlers/
     persistMessage/seed. Returns {messages, send, generating, abort,
     reset, pushMessage}. toolHandlers map fires on tool_use_stop —
     route 'patch' into htmlstudio.applyPatch and you're done.
   - frontend/src/components/chat/StandaloneChat.jsx — composes both.
     Drop into ANY surface with a Prompture-compatible endpoint:
       <StandaloneChat projectId slug toolHandlers={{patch: ...}} />
   - ChatSidebar.jsx becomes a 60-line wrapper that adds AgentSite's
     tweak-mode banner + Create-new-design button, then defers to
     <ChatPanel>. PageBuilderPage keeps its complex state setup.

2. Right-rail [Inspector | Blocks] tabs
   - frontend/src/components/builder/RightRail.jsx — tabbed wrapper.
     Auto-flips to Inspector when something gets selected; user can
     manually flip back to Blocks. Inspector tab disabled when nothing
     selected (defaults to Blocks).
   - frontend/src/components/builder/BlocksPanel.jsx — inline (non-
     modal) version of the deleted BlockPalette. Same Built-in vs
     Project tabs, 1-up grid sized for the narrow rail.
   - EditInspector loses its own <aside> wrapper (RightRail provides
     the chrome) — outer becomes a <div className="h-full ...">
   - BlockPalette.jsx DELETED — modal was the wrong shape.
   - "+ Block" toolbar button DELETED — Blocks tab replaces it.

3. SVG wireframes (htmlstudio v0.4.0)
   - BlockDefinition gains optional `wireframe` field (inline SVG).
   - 4 hand-drawn wireframes for builtins (hero-split / cta-banner /
     feature-grid-3 / testimonial-quote). 160×90, currentColor strokes
     so the palette themes them.
   - generateWireframe(template) auto-generates from tag structure
     (image presence, heading count, paragraph count, CTA presence).
   - wireframeFor(def) — convenience: authored or auto.
   - BlocksPanel cards render wireframes via dangerouslySetInnerHTML
     in a 16:9 aspect container. Emoji thumbnail field kept for back-
     compat / fallback.
   - 4 new tests; 62/62 passing.

4. "Create new design" button
   - Replaces the implicit isFirstBrief guess. Always opens the
     DiscoveryForm on a fresh slate via a new forceDiscovery state.
   - Button is a brand→purple gradient at the top of the chat sidebar,
     hidden in edit mode (banner takes priority) and while a form is
     already showing.
   - handleDiscoverySubmit/Skip now gracefully handle the "no pending
     message — brief IS the input" case.

Plan reference: previous turn's plan-then-execute pass.
Same root cause as the Dockerfile fix in 087fc4f / phase 2:
package-lock.json is generated on the Windows host, so it never
records the Linux rollup native binary (@rollup/rollup-linux-x64-gnu).
`npm ci` is strict and refuses to install anything not in the lockfile
→ build dies with "Cannot find module @rollup/rollup-linux-x64-gnu".

Fix: in both GHA workflows (dev.yml + publish.yml), rm the lockfile +
node_modules before installing, and use `npm install` instead of
`npm ci`. Lets npm pick the right platform binaries.

Tradeoff: lockfile is no longer authoritative for CI builds. Acceptable
for now since the only consumer is the frontend bundle and the bundle
is determined by package.json semver constraints. If we ever want a
fully-pinned CI install, the right answer is to ALSO generate the
lockfile on Linux (e.g. via a devcontainer or a one-off `docker run`)
and commit that.

Refs: npm/cli#4828
@jhd3197
jhd3197 merged commit 9c8fa97 into main May 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants