Sync third-party and MCP marketplace plugins
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed.
This commit is contained in:
@@ -4,18 +4,6 @@
|
||||
"displayName": "EAPIL 技能市场"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "superpowers",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/superpowers"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "ui-ux-pro-max",
|
||||
"source": {
|
||||
@@ -88,42 +76,6 @@
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "shadcn",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/shadcn"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "ppt-master",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/ppt-master"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "文档处理"
|
||||
},
|
||||
{
|
||||
"name": "mcp-playwright",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/mcp-playwright"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "MCP"
|
||||
},
|
||||
{
|
||||
"name": "docx",
|
||||
"source": {
|
||||
@@ -291,6 +243,54 @@
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "多媒体与生成"
|
||||
},
|
||||
{
|
||||
"name": "superpowers",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/superpowers"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "shadcn",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/shadcn"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "ppt-master",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/ppt-master"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "文档处理"
|
||||
},
|
||||
{
|
||||
"name": "mcp-playwright",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/codex/plugins/mcp-playwright"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "MCP"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"repo": "https://github.com/obra/superpowers.git",
|
||||
"ref": "main",
|
||||
"adapter": "codex-plugin",
|
||||
"commit": "6fd4507659784c351abbd2bc264c7162cfd386dc",
|
||||
"syncedAt": "2026-06-10T07:28:13Z"
|
||||
"commit": "8cf39006140a743dce31ba4046fceab90cc214e6",
|
||||
"syncedAt": "2026-06-16T16:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "oh-my-codex",
|
||||
@@ -33,8 +33,8 @@
|
||||
"repo": "https://github.com/shadcn-ui/ui.git",
|
||||
"ref": "main",
|
||||
"adapter": "claude-skill",
|
||||
"commit": "9197676b3d09b77f51b17b63e5978db29e498ca4",
|
||||
"syncedAt": "2026-06-15T15:59:59Z"
|
||||
"commit": "6ec117715f8519326f0ae8c9984b930f313bdb71",
|
||||
"syncedAt": "2026-06-16T16:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "frontend-slides",
|
||||
@@ -69,8 +69,8 @@
|
||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||
"ref": "main",
|
||||
"adapter": "claude-skill",
|
||||
"commit": "45d9a79874d8700583feb60ddfbca46df437864b",
|
||||
"syncedAt": "2026-06-15T15:59:59Z"
|
||||
"commit": "886c159db5bfb693ab8b228b03a1ace75aacd630",
|
||||
"syncedAt": "2026-06-16T16:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "next-skills",
|
||||
|
||||
@@ -4,18 +4,6 @@
|
||||
"displayName": "EAPIL 技能市场"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "superpowers",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/superpowers"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "ui-ux-pro-max",
|
||||
"source": {
|
||||
@@ -88,42 +76,6 @@
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "shadcn",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/shadcn"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "ppt-master",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/ppt-master"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "文档处理"
|
||||
},
|
||||
{
|
||||
"name": "mcp-playwright",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/mcp-playwright"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "MCP"
|
||||
},
|
||||
{
|
||||
"name": "docx",
|
||||
"source": {
|
||||
@@ -291,6 +243,54 @@
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "多媒体与生成"
|
||||
},
|
||||
{
|
||||
"name": "superpowers",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/superpowers"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "shadcn",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/shadcn"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "开发工具"
|
||||
},
|
||||
{
|
||||
"name": "ppt-master",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/ppt-master"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "文档处理"
|
||||
},
|
||||
{
|
||||
"name": "mcp-playwright",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/mcp-playwright"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "MCP"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"name": "playwright浏览器自动化操作",
|
||||
"version": "20260605",
|
||||
"keySource": "none",
|
||||
"syncedAt": "2026-06-15T16:01:15Z"
|
||||
"syncedAt": "2026-06-16T16:01:27Z"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "ppt-master",
|
||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||
"ref": "main",
|
||||
"commit": "45d9a79874d8700583feb60ddfbca46df437864b",
|
||||
"commit": "886c159db5bfb693ab8b228b03a1ace75aacd630",
|
||||
"adapter": "claude-skill",
|
||||
"sourcePath": "skills/ppt-master",
|
||||
"syncedAt": "2026-06-15T15:59:59Z"
|
||||
"syncedAt": "2026-06-16T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -193,16 +193,20 @@ The architecture has three independent reference bundles. Full schema in [`docs/
|
||||
|
||||
| User path's `kind` | Step 3 action |
|
||||
|---|---|
|
||||
| `kind: brand` | Copy `design_spec.md` + logo files + asset subdirs (`images/` / `illustrations/` / `icons/`) into `<project>/templates/`. Strategist locks identity segment as truth; structure stays free. |
|
||||
| `kind: layout` | Copy `design_spec.md` + SVG roster + asset files into `<project>/templates/`. Strategist locks structure; identity decided in Eight Confirmations e–g. |
|
||||
| `kind: deck` | Copy everything (`design_spec.md` + SVGs + logos + assets) into `<project>/templates/`. Strategist locks all segments; Eight Confirmations narrows to deck-content fields (audience / page count / outline / tone tweaks). |
|
||||
| `kind: brand` | `design_spec.md` + non-image assets → `<project>/templates/`; logo / illustration / icon **bitmaps** → `<project>/images/`. Strategist locks identity segment as truth; structure stays free. |
|
||||
| `kind: layout` | `design_spec.md` + SVG roster → `<project>/templates/`; any **bitmap** assets → `<project>/images/`. Strategist locks structure; identity decided in Eight Confirmations e–g. |
|
||||
| `kind: deck` | `design_spec.md` + template SVGs → `<project>/templates/`; logos / backgrounds / other **bitmaps** → `<project>/images/`. Strategist locks all segments; Eight Confirmations narrows to deck-content fields (audience / page count / outline / tone tweaks). |
|
||||
|
||||
```bash
|
||||
TEMPLATE_DIR=<user-supplied path>
|
||||
# Bitmaps join the project's single runtime image pool (images/, referenced as
|
||||
# ../images/); the spec + template SVGs + other non-image assets stay in
|
||||
# templates/ as design reference the Strategist/Executor read but never render.
|
||||
cp -r ${TEMPLATE_DIR}/* <project_path>/templates/
|
||||
find <project_path>/templates -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.gif' -o -iname '*.webp' -o -iname '*.bmp' \) -exec mv {} <project_path>/images/ \;
|
||||
```
|
||||
|
||||
The single-line copy suffices for all three kinds — the spec's `kind` field tells Strategist how to read it; downstream code doesn't distinguish.
|
||||
The same split applies to all three kinds — bitmaps always land in `images/`, the rest in `templates/`. The spec's `kind` field tells Strategist how to read the `templates/` side; downstream code doesn't distinguish. (Template SVGs in `templates/` are reference material only — the rendered pages live in `svg_output/` and reference images via `../images/`.)
|
||||
|
||||
#### Multi-path fusion
|
||||
|
||||
@@ -292,9 +296,14 @@ Read references/strategist.md
|
||||
```bash
|
||||
python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --daemon --wait
|
||||
```
|
||||
Page opens at `http://localhost:4040`; port conflict → `--port <other>` and report the actual URL. When the user clicks **Confirm**, the command exits 0 and Step 4 reads `result.json` immediately; do not require a second chat confirmation. **Launch or wait failure is non-fatal**: if it fails or times out (flask missing, port blocked, no GUI / remote / web host, browser never confirms in time), do **NOT** troubleshoot. The detached page stays open, so a slow user may confirm after the wait returns — therefore **on any non-zero exit, re-check `<project_path>/confirm_ui/result.json` once (a fresh `status: confirmed`) before** dropping to the chat-summary fallback below.
|
||||
Page opens at `http://localhost:5050` — the **same port as the Step 6 live preview** (they never run at once: this page shuts down at the end of Step 4, freeing the port). If another project already holds 5050, the launcher **auto-advances to the next free port** (5051, …) and serves this project there — read the actual URL from the launch log and report that. When the user clicks **Confirm**, the command exits 0 and Step 4 reads `result.json` immediately; do not require a second chat confirmation. **Launch or wait failure is non-fatal**: if it fails or times out (flask missing, port blocked, no GUI / remote / web host, browser never confirms in time), do **NOT** troubleshoot. The detached page stays open, so a slow user may confirm after the wait returns — therefore **on any non-zero exit, re-check `<project_path>/confirm_ui/result.json` once (a fresh `status: confirmed`) before** dropping to the chat-summary fallback below.
|
||||
3. **Always also print the eight recommendations as a short summary in chat, with the URL.** This keeps the chat fallback valid whether or not the browser opened. If the page never appears, the user simply confirms or edits in chat as before.
|
||||
4. This is the ⛔ BLOCKING wait. Preferred page path: the `--wait` command returns after the page writes a fresh `<project_path>/confirm_ui/result.json`; immediately read that file and use its values. On a non-zero exit, re-check `result.json` once (per step 2) — a fresh `status: confirmed` still wins. Chat fallback path: only if no fresh result exists (page didn't open, wait timed out with no confirmation, or the user replies in chat with edits) take the chat values directly. Either path converges. A confirmed `result.json` is an explicit user choice: `generation_mode: "split"` means split mode was chosen; `refine_spec: true` means the refine-spec workflow was chosen.
|
||||
5. **Close the confirm page (Mandatory cleanup — every path).** Once you have the confirmed values (page **or** chat), shut the confirm server down before leaving Step 4 so it cannot keep holding port 5050 (which Step 6 live preview reuses):
|
||||
```bash
|
||||
python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --shutdown
|
||||
```
|
||||
This is **idempotent and required regardless of whether Confirm was clicked**: clicking Confirm already shuts the page down (this is then a no-op), but the chat-fallback path leaves the page running — without this cleanup it would block the live preview launch. Run it after reading the confirmation and before proceeding to Step 5.
|
||||
|
||||
**Honoring the confirmation (result.json is authoritative — Mandatory)**: the confirmed values **override your own recommendations** when you write `design_spec.md` / `spec_lock.md`. A user who changed any field changed it on purpose. In particular, map `image_usage` to §VIII `Acquire Via` (its value names differ from §h options — translate):
|
||||
|
||||
@@ -391,6 +400,8 @@ A deck with only `ai` rows never loads `image-searcher.md`; a deck with only `we
|
||||
|
||||
> ⚠️ **In-pipeline ai path MUST use manifest mode** — even when only 1 ai row exists. Write `images/image_prompts.json` first, then run `image_gen.py --manifest`, then `image_gen.py --render-md` to produce the `image_prompts.md` sidecar. The positional form (`image_gen.py "prompt" ...`) is reserved for **out-of-pipeline one-off testing / single-image fixups** — it skips manifest + sidecar, leaving no audit trail.
|
||||
|
||||
> ⚠️ **Honor the confirmed image source**: the `ai` generation path (Path A = `image_gen.py` API / Path B = host-native tool / Offline Manual) is **not** auto-only — a confirmed choice other than `auto` wins, whether it came from chat (canonical) or, when the page was used, `result.json.image_ai_path`. `host-native` forces Path B even when `IMAGE_BACKEND` is configured; `api` forces Path A; `manual` forces offline. The `--manifest` command above is Path A. Full selection rule: [image-generator.md](references/image-generator.md) §7 Path Selection.
|
||||
|
||||
Workflow:
|
||||
|
||||
1. Extract all rows with `Status: Pending` and `Acquire Via ∈ {ai, web}` from the design spec
|
||||
@@ -439,7 +450,7 @@ Read references/visual-styles/<locked-style>.md # aesthetic (spec_lock.md `vis
|
||||
```bash
|
||||
python3 ${SKILL_DIR}/scripts/svg_editor/server.py <project_path> --live
|
||||
```
|
||||
- Start it immediately when Executor begins; `svg_output/` may be empty. Editor opens at `http://localhost:5050`; port conflict → `--port <other>` and report the actual URL.
|
||||
- Start it immediately when Executor begins; `svg_output/` may be empty. Editor opens at `http://localhost:5050`; if another project already holds it, the launcher **auto-advances to the next free port** — read the actual URL from the launch log and report that.
|
||||
- Run it as a long-running side process/session; do not wait for it to exit before generating SVG pages. Do not wait for user confirmation after startup.
|
||||
- **Service must keep running** until one of: (a) the user clicks **Exit preview** in the browser, or (b) the user explicitly asks in chat to stop it. Generation continues even if the user closes the editor.
|
||||
- **Do NOT read or apply submitted annotations during generation.** Users may annotate at any time, but Executor proceeds without touching them. The window to apply annotations opens only after Step 7 completes — see [`workflows/live-preview.md`](workflows/live-preview.md).
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
| Source list | Read path |
|
||||
|---|---|
|
||||
| Chosen template's `design_spec.md` (read frontmatter to detect `replication_mode`) | `templates/<chosen_template>/design_spec.md` |
|
||||
| Every distinct `<basename>` in `spec_lock.md page_layouts` | `templates/<chosen_template>/<basename>.svg` |
|
||||
| Chosen template's `design_spec.md` (read frontmatter to detect `replication_mode`) | `templates/design_spec.md` |
|
||||
| Every distinct `<basename>` in `spec_lock.md page_layouts` | `templates/<basename>.svg` |
|
||||
| Every distinct chart name in `spec_lock.md page_charts` | `templates/charts/<chart_name>.svg` |
|
||||
| Chart types in `design_spec.md §VII` not covered above | `templates/charts/<chart_name>.svg` |
|
||||
|
||||
@@ -32,7 +32,7 @@ Resolve the per-page template SVG via `spec_lock.md page_layouts` (authoritative
|
||||
**Resolution order (per page):**
|
||||
|
||||
1. **Mirror-mode template** (template's `design_spec.md` frontmatter has `replication_mode: mirror`) → see §1.1 below. The page is consumed as a **visual reference**, not as a placeholder shell.
|
||||
2. `spec_lock.md page_layouts` has `P<NN>: <basename>` for this page → inherit the structure of `templates/<chosen_template>/<basename>.svg` (already in context from §1.0).
|
||||
2. `spec_lock.md page_layouts` has `P<NN>: <basename>` for this page → inherit the structure of `templates/<basename>.svg` (already in context from §1.0).
|
||||
3. `page_layouts` exists but **no entry** for this page → **free design**, no template inheritance.
|
||||
4. `page_layouts` section absent (legacy deck) **and** `templates/` directory exists → fall back to the page-type table below, matching by SVG filename keyword (cover/chapter/content/ending/toc). Read the matched file at first use if §1.0 batch did not cover it.
|
||||
5. No template at all → free design.
|
||||
@@ -46,9 +46,9 @@ Resolve the per-page template SVG via `spec_lock.md page_layouts` (authoritative
|
||||
When the project's chosen template is a `mirror` template (`design_spec.md` frontmatter declares `replication_mode: mirror`), Executor switches to a **reference-style** consumption path that bypasses placeholder substitution:
|
||||
|
||||
1. **Per-page reference selection** — Strategist selects one mirror page per project page via `spec_lock.md page_layouts` (e.g., `P04: 015_content`). The basename is the mirror filename without extension; Strategist made this choice by reading `design_spec.md §V Page Roster` descriptions, not by guessing.
|
||||
2. **Copy, don't fill** — open the referenced mirror SVG (already in context from §1.0). **Copy it as the starting point for the project page**, then edit text elements in place to express the project's content for `P<NN>`. Preserve every non-text element verbatim: backgrounds, decorative shapes, sprite-cropped images, charts, icon usage, color values, font families, geometry, sprite `<svg viewBox>` wrappers, `<image>` references.
|
||||
2. **Copy, don't fill** — open the referenced mirror SVG (already in context from §1.0). **Copy it as the starting point for the project page**, then edit text elements in place to express the project's content for `P<NN>`. Preserve every non-text element verbatim: backgrounds, decorative shapes, sprite-cropped images, charts, icon usage, color values, font families, geometry, sprite `<svg viewBox>` wrappers, and **which image** each `<image>` points at.
|
||||
3. **What you may edit** — the visible text content of `<text>` / `<tspan>` elements that express slide-specific content (title, body, captions, KPI labels, dates, page numbers). Replace the source deck's example text with the project's text for this page from `design_spec.md §IX` and `notes/<NN>_*.md`.
|
||||
4. **What you must not touch** — element positions, sizes, fonts, colors, fills, strokes, gradients, image hrefs, `<g>` grouping, sprite-sheet `<svg viewBox>` wrappers, decorative `<rect>` / `<path>` / `<circle>` / `<polygon>` shapes, `<use data-icon="...">` markers, embedded chart data structures. Mirror's value is preserving the source deck's visual identity — any geometric / decorative drift defeats the purpose.
|
||||
4. **What you must not touch** — element positions, sizes, fonts, colors, fills, strokes, gradients, **which image each `<image>` points at**, `<g>` grouping, sprite-sheet `<svg viewBox>` wrappers, decorative `<rect>` / `<path>` / `<circle>` / `<polygon>` shapes, `<use data-icon="...">` markers, embedded chart data structures. Mirror's value is preserving the source deck's visual identity — any geometric / decorative drift defeats the purpose. **The `href` path is not the image**: normalizing a bare `href="cover_bg.png"` to `href="../images/<name>"` (when Step 3 relocated the asset to `images/`) points at the *same* image and changes nothing visual — that is an allowed path fix, not a fidelity edit. Leaving the bare href as-is is also fine; the exporter and live preview resolve bare hrefs against `images/` either way.
|
||||
5. **Content fit** — the mirror page was chosen by Strategist because its layout matches the content slot. If the project's content for `P<NN>` legitimately needs more / fewer items than the mirror page provides (e.g. mirror shows 3 KPI cards, project has 4 metrics), keep the mirror page's visual rhythm and either drop one metric to fit or split across two pages — do **not** restructure the mirror page's grid. If neither works, surface a `warning: P<NN> content does not fit mirror reference <basename>; suggest different reference page` and proceed with the closest-fit edit.
|
||||
6. **No `{{}}` substitution** — mirror SVGs do not contain placeholder markers. Do not search for `{{TITLE}}` / `{{CONTENT_AREA}}` etc.; do not invent placeholders. The whole mirror contract is "verbatim source + in-place text edit".
|
||||
7. **Output filename** — follow the standard project SVG naming convention (`<NN>_<page_name>.svg` where `<NN>` matches the project page index, not the mirror source index). The mirror filename is the *reference*, not the *output*.
|
||||
@@ -72,7 +72,7 @@ When the project's chosen template is a `mirror` template (`design_spec.md` fron
|
||||
Before generating each page, output which template is used:
|
||||
|
||||
```
|
||||
📝 **Template mapping**: `templates/<chosen_template>/03a_content_image_text.svg` (or "None (free design)")
|
||||
📝 **Template mapping**: `templates/03a_content_image_text.svg` (or "None (free design)")
|
||||
🎯 **Adherence rules / layout strategy**: [specific description]
|
||||
```
|
||||
|
||||
@@ -323,6 +323,8 @@ Handle images by their status in the Design Spec's Image Resource List. Status e
|
||||
|
||||
**Reference syntax**: see [`svg-image-embedding.md`](svg-image-embedding.md).
|
||||
|
||||
**Template-bundled images**: when a template (deck / layout / brand) is applied, its bitmaps are copied into the project's `images/` alongside every other runtime image (SKILL.md Step 3). Reference them the same way — `../images/<name>` — and do **not** reproduce a template SVG's bare sibling href (e.g. `href="cover_bg.png"`): the template SVG is reference material, the rendered page lives in `svg_output/` and must point at `../images/`. Mirror templates (§1.1) are the one exception — they copy hrefs verbatim, and the exporter resolves those bare hrefs against `images/`.
|
||||
|
||||
**Placeholder**: Dashed border `<rect stroke-dasharray="8,4" .../>` + description text
|
||||
|
||||
**`no-crop` images**: when a `spec_lock.md images` entry ends with ` | no-crop`, size the container to the image's native ratio (from `analyze_images.py` or file dims) and use `preserveAspectRatio="xMidYMid meet"`. Untagged entries are croppable — default to `slice`.
|
||||
|
||||
@@ -417,14 +417,17 @@ C (AI-generated) supports three implementation modes sharing one `image_prompts.
|
||||
| `IMAGE_BACKEND` not configured (or Path A fails) AND host has a native image tool | **Path B**: Host-native tool | Agent invokes the host's image capability; outputs land at `project/images/<filename>` |
|
||||
| **Both Path A and Path B fail/unavailable** | **Offline Manual Mode** | Manifest stays on disk; user generates externally from `items[].prompt` and places files at `project/images/<filename>` |
|
||||
|
||||
**Selection logic** — monotonic A → B → C fallback chain (automatic, no user prompting):
|
||||
**Selection logic** — the confirmed user choice wins; absent one, fall back to the automatic A → B → C chain:
|
||||
|
||||
0. **Confirmed override (wins)** — honor the confirmed image source. The **chat choice is canonical**; the Confirm UI is only a convenience surface that, when used, records the same choice to `<project>/confirm_ui/result.json` as `image_ai_path` (so there is no `result.json` on the chat path — read the choice from the conversation). From either channel, if the choice is set and not `auto`, honor it directly, **even when it contradicts `IMAGE_BACKEND`**:
|
||||
- `api` → **Path A** (`image_gen.py --manifest`).
|
||||
- `host-native` → **Path B** (host's native image tool) — skip A *even if `IMAGE_BACKEND` is configured*.
|
||||
- `manual` → **Offline Manual** (write prompts, hand off).
|
||||
("use Codex's image tool" / "走接口生成" in chat = `host-native` / `api`.) If the chosen path turns out unavailable (e.g. `host-native` but the host has no image tool), fall through along the chain below from that point. Only when no source named a path (chat silent, and `image_ai_path` `auto` / absent) does the automatic chain decide.
|
||||
1. **Try Path A** — if `IMAGE_BACKEND` is configured (env or `.env`), run `image_gen.py --manifest`. If it fails twice in a row, fall to Path B.
|
||||
2. **Try Path B** — if `IMAGE_BACKEND` was not configured (A skipped), or A failed, and the host has a native image tool (Codex / Antigravity / Claude Code / similar), the agent invokes the host's image capability directly.
|
||||
3. **Fall to C (Offline Manual)** — if B is also unavailable (no host-native tool) or fails, write prompts to `images/image_prompts.json` and hand off to the user.
|
||||
|
||||
**User override**: If the user explicitly names Path B ("use Codex's image tool"), skip A and start at B. Explicit naming is the only way to bypass an earlier path in the chain; otherwise the chain is monotonic.
|
||||
|
||||
**Hard rule**: Step 4 is execution, not re-decision. Never present an interactive choice between paths here — image strategy was locked in Strategist Step 4 h item.
|
||||
|
||||
> All three modes share one output contract: file at `project/images/<filename>`. Step 6 SVG references are mode-agnostic.
|
||||
|
||||
@@ -71,6 +71,8 @@ The deck's **visual aesthetic** — shape language, decoration density, whitespa
|
||||
- User named a style → map to the closest preset; if none fits, `custom` with a `visual_style_behavior` paragraph.
|
||||
- No user description → recommend by the index's auto-selection table (content vibe / industry → style). Present as a recommendation; the user may override.
|
||||
|
||||
**Forbidden — a non-catalog name as `visual_style`**: the value MUST be an `id` from the visual-styles catalog (or genuine `custom` prose). A name that is **not** in that catalog is not a visual style — most often it is an image-rendering name from the `_index` "Paired rendering" column (`flat`, `vector-illustration`, `digital-dashboard`, `3d-isometric`, `corporate-photo`, …), which names the §h *illustration* family, not the deck's layout aesthetic. Do not borrow it. (Names that are intentionally **both** a style and its paired rendering — `glassmorphism`, `blueprint`, `editorial`, `dark-tech` — are valid styles because they *are* in the catalog.) Generic baseline words — `flat` / flat-design / 扁平 / modern / clean / simple / minimal — are **not** custom-worthy either: the whole system is flat by default (shadows discouraged), so map them to the closest preset (flat + grid → `swiss-minimal`; flat + rounded → `soft-rounded`; flat + dense → `brutalist`). Reserve `custom` for an aesthetic no preset covers.
|
||||
|
||||
**Carries no color.** A visual style governs how the deck's HEX (locked at `e`) is *used* — never which colors, same discipline as [`image-renderings`](./image-renderings/_index.md). When the deck has AI images, prefer the style's paired rendering so layout and illustration share one aesthetic.
|
||||
|
||||
Write the locked value to `spec_lock.md` `- visual_style:` and the rationale to `design_spec.md`. Executor loads only that one visual-style file.
|
||||
@@ -143,6 +145,8 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
|
||||
|
||||
### g. Typography Plan Confirmation (Font + Size)
|
||||
|
||||
🚧 **GATE — read the locked style's type character first**: `read_file` the visual-style file locked at §d Layer 2 (`visual-styles/<visual_style>.md`) and pull its **§2 Typography character** (you only read the catalog index there; the per-style character lives in the file). Both combinations below MUST realize it, and the **title carries the personality** — the CJK body may stay a neutral pre-installed sans, but the title leads with the character the style asks for (e.g. `ink-wash` → calligraphic `KaiTi` / `FangSong`; `brutalist` / `memphis` / `vintage-poster` / `zine` → display `SimHei` / `Impact`; `editorial` / `data-journalism` / `photo-editorial` → serif `Georgia` / `Cambria` / `SimSun`; `dark-tech` / `blueprint` → clean sans + `Consolas` mono; `swiss-minimal` / `soft-rounded` → grotesque / friendly sans). For `visual_style: custom`, realize its `visual_style_behavior` character instead. Letting the title default to a neutral sans when the style asks for character is the failure mode to avoid.
|
||||
|
||||
#### Font Combinations
|
||||
|
||||
> Same-deck fonts must form **contrast** (different family, weight, or proportion) or **concord** (one family throughout). "Similar but not identical" pairings *across roles* are forbidden — see blacklist below. *Within one stack*, pairing a Windows font with a macOS counterpart (e.g. `Microsoft YaHei` + `PingFang SC`) is encouraged as a browser-preview nicety; converter writes only the first into PPTX.
|
||||
@@ -164,7 +168,7 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
|
||||
- `"Times New Roman"` ↔ `Times`
|
||||
- `Georgia` ↔ `Cambria`
|
||||
|
||||
**Mandatory**: propose **two** combinations to the user — one concord (safe), one contrast (with tension). Do not default to "title = body, same font" without explicit user request. Pick each family by subject fit and the locked `visual_style` character — there is **no default family**; type should follow the deck's content and aesthetic, not fall back to one safe face.
|
||||
**Mandatory**: propose **two** combinations to the user — one concord (safe), one contrast (with tension). Do not default to "title = body, same font" without explicit user request. Pick each family by subject fit and the locked `visual_style`'s **§2 character** (read at the GATE above) — there is **no default family**; type should follow the deck's content and aesthetic, not fall back to one safe face.
|
||||
|
||||
> **Template precedence**: when a template was loaded at Step 3 via an explicit path and declares `title` / `body` font stacks in `<project_path>/templates/design_spec.md §III Typography` / §IV (or whichever heading the fused spec uses), lock those directly and skip the two-combination presentation. Same precedence as e. — user override > template values.
|
||||
|
||||
@@ -179,7 +183,7 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
|
||||
| Mono | Consolas, Courier New |
|
||||
| Display | Impact, Arial Black |
|
||||
|
||||
**Seed combinations** (all PPT-safe; first column is the contrast axis, not a scenario) — starting points, not the allowed set. Any client-preinstalled family is fair game; non-pre-installed expressive faces go title-only (see note below).
|
||||
**Seed combinations** (all PPT-safe; first column is the contrast axis, not a scenario) — starting points, not the allowed set. Any client-preinstalled family is fair game; non-pre-installed expressive faces go title-only (see note below). Let the locked style's §2 character pick the axis and the title lead; the `Microsoft YaHei` body cells are the **neutral default, not a requirement** — a styled deck still varies the title even when the CJK body stays a neutral sans.
|
||||
|
||||
| Contrast axis | Title stack | Body stack | Code stack |
|
||||
|---|---|---|---|
|
||||
@@ -194,7 +198,7 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
|
||||
| Tech / developer | `Arial, "Microsoft YaHei", sans-serif` | same | `Consolas, "Courier New", monospace` |
|
||||
| Concord (single family — pick the family by subject + `visual_style`) | `<family by subject>, …, sans-serif / serif` | same | — |
|
||||
|
||||
> **Stack length discipline (soft rule).** ≤4 fonts per stack. Lead with Windows-preinstalled fonts (Microsoft YaHei / SimSun / Arial / Georgia / Consolas); keep at most **one** macOS-exclusive family (typically `"PingFang SC"`). Converter only picks the first Latin and first CJK font ([`drawingml_utils.py parse_font_family`](../scripts/svg_to_pptx/drawingml_utils.py)); macOS→Windows fallback is auto-mapped via `FONT_FALLBACK_WIN`.
|
||||
> **Stack length discipline (soft rule).** ≤4 fonts per stack. The **first** CJK and first Latin font MUST be pre-installed — the converter writes only those, and a non-installed lead substitutes to Calibri ([`drawingml_utils.py parse_font_family`](../scripts/svg_to_pptx/drawingml_utils.py)). Choose that lead from the safe set **by the locked style's character**: `Microsoft YaHei` / `Arial` are the *neutral* members — perfect as the tail fallback and as the lead only when the style is plain-sans, but **not the automatic lead for every deck**. For a styled title, lead with `SimHei` / `KaiTi` / `FangSong` / `SimSun` / `Georgia` / `Cambria` / `Impact` / `Consolas` as the character asks. Keep at most **one** macOS-exclusive family (typically `"PingFang SC"`); macOS→Windows fallback is auto-mapped via `FONT_FALLBACK_WIN`.
|
||||
|
||||
> **Non-pre-installed directions** (require install or PPTX embed; note the constraint in Design Spec):
|
||||
> - **Retro / pixel** — Press Start 2P / VT323 / Silkscreen
|
||||
@@ -336,7 +340,7 @@ After the candidates, append one line:
|
||||
| Span a personality spectrum | Typically: one conservative-default (industry norm), one shifted-tone (same fit, 1-2 ticks different), one bold-contrast (more expressive, may challenge default). No near-duplicates. |
|
||||
| `Mood` line MUST include a real-world analogy | Company / publication / event the user can picture. Adjective stacks alone are forbidden. |
|
||||
| Adapt labels to chat language | Schema is English by default. Chinese chat → render as 「方案 A / 视觉 / 色彩 / 情绪」. Structure stays the same; only the labels translate. |
|
||||
| Skip presentation when user has specified | User-named rendering or palette (chat / brand / template) bypasses the candidate flow — lock directly per the truth-precedence rule. |
|
||||
| Skip presentation when user has specified | User-named rendering or palette (chat / brand / template), **or a Confirm UI pick in `result.json.image_strategy`** (same shape as color / typography honoring their confirmed candidate), bypasses the candidate flow — lock *that chosen candidate's* `rendering` + `palette` directly per the truth-precedence rule; do not re-pick. |
|
||||
| `custom` is a tail-case, not a default | When no preset fits, a candidate may set `rendering: custom` and / or `palette: custom` (rules: [`image-renderings/_index.md`](../image-renderings/_index.md) §1.5, [`image-palettes/_index.md`](../image-palettes/_index.md) §2). At most one candidate per dimension may carry `custom`; one candidate may carry both dimensions as `custom`. `Visual` / `Color` lines describe the behavior in prose, never by naming a competing preset. |
|
||||
|
||||
**Forbidden — padding with conflicts**: if e.'s HEX cannot find ≥3 compatible palettes, present the smaller set (2 candidates) and state "your color is unusual — only N palettes can carry it without conflict." A `custom` candidate is allowed only when its prose genuinely describes a tail-case the presets cannot — not as a slot-filler. Never fill remaining slots with known-conflicting options.
|
||||
|
||||
@@ -354,7 +354,7 @@ If the template is based on PPTX import output, briefly note:
|
||||
|
||||
If suitable template resources already exist, use them directly instead of generating new ones:
|
||||
|
||||
1. **Copy template**: Copy template files to the project's `templates/` directory
|
||||
1. **Copy template**: copy the spec + template SVGs into the project's `templates/`, and any bundled bitmaps into the project's `images/` (the runtime image pool, referenced as `../images/`)
|
||||
2. **Adjust colors**: Modify colors per the project design spec
|
||||
3. **Customize**: Make project-specific adjustments
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ A **visual style** is how the deck **looks** — shape language, decoration dens
|
||||
|
||||
Each style has its own file with: shape & decoration, typography character, color-usage discipline (no HEX), texture / elevation, and the paired image-rendering. **Read only the file for the style you lock** — never glob the directory. The catalog mirrors [`image-renderings`](../image-renderings/_index.md): each style's "Paired rendering" names the illustration family that shares its aesthetic.
|
||||
|
||||
> The **`visual_style` value is only ever a first-column `id`** (`swiss-minimal`, `editorial`, …). The "Paired rendering" column lists **§h image-rendering** names (`flat`, `minimalist-swiss`, `digital-dashboard`, …) — never lock one of those as the `visual_style`; they belong to confirmation h.
|
||||
|
||||
### 1.1 Corporate / product
|
||||
|
||||
| Visual style | Character | Best for | Paired rendering |
|
||||
|
||||
@@ -20,7 +20,7 @@ Usage:
|
||||
|
||||
Examples:
|
||||
python3 scripts/confirm_ui/server.py projects/my-project
|
||||
python3 scripts/confirm_ui/server.py projects/my-project --port 4041
|
||||
python3 scripts/confirm_ui/server.py projects/my-project --port 5051
|
||||
python3 scripts/confirm_ui/server.py projects/my-project --no-browser
|
||||
python3 scripts/confirm_ui/server.py projects/my-project --daemon --wait
|
||||
|
||||
@@ -38,6 +38,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@@ -51,6 +52,7 @@ if str(_SCRIPTS_DIR) not in sys.path:
|
||||
|
||||
from server_common import ( # noqa: E402
|
||||
claim_lock as _claim_lock,
|
||||
find_free_port as _find_free_port,
|
||||
process_alive as _process_alive,
|
||||
read_lock as _read_lock,
|
||||
release_lock as _release_lock,
|
||||
@@ -68,7 +70,14 @@ CONFIRM_DIR_NAME = 'confirm_ui'
|
||||
RECOMMENDATIONS_NAME = 'recommendations.json'
|
||||
RESULT_NAME = 'result.json'
|
||||
|
||||
DEFAULT_PORT = 4040
|
||||
# Static option universe served at /api/catalogs (canvas synced live from config).
|
||||
_CATALOGS_PATH = Path(__file__).resolve().parent / 'static' / 'catalogs.json'
|
||||
|
||||
# Shares port 5050 with the live preview server (svg_editor/server.py). The two
|
||||
# never run at once: confirm is Step 4 and shuts down on confirm (or idle),
|
||||
# freeing the port before live preview starts at Step 6. One port = one forward
|
||||
# rule for the whole pipeline. They still keep separate processes and locks.
|
||||
DEFAULT_PORT = 5050
|
||||
|
||||
# Default --wait budget, kept just under the 600s Bash-tool ceiling so the
|
||||
# parent (waiting) command returns before the calling harness kills it. The
|
||||
@@ -115,6 +124,86 @@ def _wait_for_result(
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def _shutdown_existing(lock_file: Path) -> int:
|
||||
"""Stop a confirm server left running for this project (idempotent).
|
||||
|
||||
Step 4 always calls this on exit so the page never lingers on the shared
|
||||
port 5050 — whether the user clicked **Confirm** (the page already shut the
|
||||
server down) or replied in chat instead (the server is still up). Tries a
|
||||
graceful ``/api/shutdown`` first, falls back to killing the recorded pid,
|
||||
then clears the lock. A no-op when nothing is running.
|
||||
"""
|
||||
existing = _read_lock(lock_file)
|
||||
if not existing:
|
||||
logger.info('no confirm server running — nothing to stop')
|
||||
return 0
|
||||
pid = int(existing.get('pid', 0) or 0)
|
||||
port = existing.get('port')
|
||||
if not _process_alive(pid):
|
||||
_release_lock(lock_file)
|
||||
logger.info('confirm server already stopped; cleared stale lock')
|
||||
return 0
|
||||
# Graceful first: the server flushes and releases its own lock.
|
||||
if port:
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f'http://127.0.0.1:{port}/api/shutdown',
|
||||
data=b'{"reason": "step4-cleanup"}',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=3)
|
||||
except OSError:
|
||||
pass # server may already be exiting; fall through to the kill path
|
||||
for _ in range(20): # up to ~2s for the graceful exit to land
|
||||
if not _process_alive(pid):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
if _process_alive(pid):
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
pass
|
||||
_release_lock(lock_file)
|
||||
logger.info('confirm server stopped (pid=%s)', pid)
|
||||
return 0
|
||||
|
||||
|
||||
def _build_catalogs() -> dict:
|
||||
"""Return the static catalog set with the canvas list synced live from
|
||||
``config.CANVAS_FORMATS`` — the single source of truth for canvas formats —
|
||||
so the confirm page can never drift from the pipeline's real formats. The
|
||||
set of formats and their dimensions come from config; bilingual labels and
|
||||
use text are kept from catalogs.json (with a plain fallback for any new id).
|
||||
"""
|
||||
data = json.loads(_CATALOGS_PATH.read_text(encoding='utf-8'))
|
||||
try:
|
||||
import config # scripts/ is on sys.path (injected at import time)
|
||||
formats = config.CANVAS_FORMATS
|
||||
except (ImportError, AttributeError): # missing module/attr → static canvas
|
||||
return data
|
||||
existing = {
|
||||
c.get('id'): c
|
||||
for c in data.get('canvas', [])
|
||||
if isinstance(c, dict) and c.get('id')
|
||||
}
|
||||
canvas = []
|
||||
for cid, fmt in formats.items():
|
||||
entry = dict(existing.get(cid, {}))
|
||||
entry['id'] = cid
|
||||
entry['dim'] = fmt.get('dimensions', entry.get('dim', ''))
|
||||
if not entry.get('label'):
|
||||
name = fmt.get('name', cid)
|
||||
entry['label'] = name
|
||||
entry.setdefault('label_zh', name)
|
||||
entry.setdefault('label_en', name)
|
||||
if not entry.get('use_en') and fmt.get('use_case'):
|
||||
entry['use_en'] = fmt['use_case']
|
||||
canvas.append(entry)
|
||||
data['canvas'] = canvas
|
||||
return data
|
||||
|
||||
|
||||
# --- app --------------------------------------------------------------------
|
||||
|
||||
def create_app(
|
||||
@@ -171,6 +260,15 @@ def create_app(
|
||||
def index():
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
@app.route('/api/catalogs')
|
||||
def get_catalogs():
|
||||
"""Serve the option universe; canvas is synced live from config.py so
|
||||
the static catalogs.json copy can never drift from the real formats."""
|
||||
try:
|
||||
return jsonify(_build_catalogs())
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
return jsonify({'error': f'invalid catalogs.json: {exc}'}), 500
|
||||
|
||||
@app.route('/api/recommendations')
|
||||
def get_recommendations():
|
||||
"""Serve the Strategist-authored recommendations for this project."""
|
||||
@@ -235,6 +333,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
'--timeout', type=int, default=900,
|
||||
help='Server idle timeout in seconds (default: 900; 0 = disabled)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--shutdown', action='store_true',
|
||||
help='Stop a confirm server left running for this project, then exit '
|
||||
'(idempotent). Run at the end of Step 4 so the page never lingers '
|
||||
'on the shared port before live preview starts.',
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -253,6 +357,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
logger.error('%s is not a directory', project_path)
|
||||
return 1
|
||||
|
||||
# Step 4 cleanup: stop any lingering confirm server and exit. Independent of
|
||||
# recommendations.json (the page may never have been confirmed).
|
||||
if args.shutdown:
|
||||
return _shutdown_existing(project_path / LOCK_FILE_NAME)
|
||||
|
||||
rec_file = project_path / CONFIRM_DIR_NAME / RECOMMENDATIONS_NAME
|
||||
if not rec_file.exists():
|
||||
logger.error(
|
||||
@@ -279,12 +388,15 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
log_path = confirm_dir / 'server.log'
|
||||
result_file = confirm_dir / RESULT_NAME
|
||||
started_at = time.time()
|
||||
# Pick a free port up front (another project may hold the default) and
|
||||
# pass the concrete port to the child so the reported URL is accurate.
|
||||
port = _find_free_port(args.port)
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(Path(__file__).resolve()),
|
||||
str(project_path),
|
||||
'--port',
|
||||
str(args.port),
|
||||
str(port),
|
||||
'--timeout',
|
||||
str(args.timeout),
|
||||
]
|
||||
@@ -305,7 +417,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
creationflags=creationflags,
|
||||
**popen_kwargs,
|
||||
)
|
||||
url = f'http://localhost:{args.port}'
|
||||
url = f'http://localhost:{port}'
|
||||
logger.info('started confirm UI in background: %s (pid=%s)', url, proc.pid)
|
||||
logger.info('log: %s', log_path)
|
||||
if args.wait:
|
||||
|
||||
+33
-2
@@ -500,12 +500,29 @@
|
||||
var sec = section(5, "sec_color");
|
||||
var grid = el("div", "color-grid");
|
||||
var hexInputs = {};
|
||||
var hexSwatches = {};
|
||||
var cardSwatchRefs = []; // idx -> {role: swatchEl}, for live override feedback
|
||||
var selectedIdx = -1;
|
||||
|
||||
function normHex(val) {
|
||||
var v = (val || "").trim();
|
||||
if (!/^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(v)) return null;
|
||||
return v.charAt(0) === "#" ? v : "#" + v;
|
||||
}
|
||||
function paintSwatch(elem, val) {
|
||||
var n = normHex(val);
|
||||
elem.style.background = n || "transparent";
|
||||
elem.classList.toggle("hex-swatch-empty", !n);
|
||||
}
|
||||
function applyHexInputs(pal) {
|
||||
PALETTE_ROLES.forEach(function (role) { if (hexInputs[role]) hexInputs[role].value = pal[role] || ""; });
|
||||
PALETTE_ROLES.forEach(function (role) {
|
||||
if (hexInputs[role]) hexInputs[role].value = pal[role] || "";
|
||||
if (hexSwatches[role]) paintSwatch(hexSwatches[role], pal[role]);
|
||||
});
|
||||
}
|
||||
function selectCard(idx) {
|
||||
var c = cands[idx] || {};
|
||||
selectedIdx = idx;
|
||||
STATE.color = { name: c.name || "", palette: Object.assign({}, normPalette(c)) };
|
||||
grid.querySelectorAll(".color-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); });
|
||||
applyHexInputs(STATE.color.palette);
|
||||
@@ -513,15 +530,18 @@
|
||||
|
||||
cands.forEach(function (c, idx) {
|
||||
var pal = normPalette(c);
|
||||
var refs = {};
|
||||
var card = el("div", "color-card");
|
||||
var sw = el("div", "swatches");
|
||||
PALETTE_ROLES.forEach(function (role) {
|
||||
if (!pal[role]) return;
|
||||
var col = el("div", "swatch-col");
|
||||
var s = el("div", "swatch"); s.style.background = pal[role];
|
||||
refs[role] = s;
|
||||
col.appendChild(s); col.appendChild(el("div", "swatch-role", t("role_" + role)));
|
||||
sw.appendChild(col);
|
||||
});
|
||||
cardSwatchRefs[idx] = refs;
|
||||
card.appendChild(sw);
|
||||
card.appendChild(el("div", "color-name", localized(c, "name") || (t("option_prefix") + " " + (idx + 1))));
|
||||
if (localized(c, "note")) card.appendChild(el("div", "color-note", localized(c, "note")));
|
||||
@@ -536,14 +556,25 @@
|
||||
PALETTE_ROLES.forEach(function (role) {
|
||||
var wrap = el("div", "hex-cell");
|
||||
wrap.appendChild(el("div", "hex-cell-label", t("role_" + role)));
|
||||
var line = el("div", "hex-input-line");
|
||||
var sw = el("div", "hex-swatch hex-swatch-empty");
|
||||
var inp = document.createElement("input");
|
||||
inp.type = "text"; inp.placeholder = "#";
|
||||
inp.addEventListener("input", function () {
|
||||
if (!STATE.color) STATE.color = { name: "custom", palette: {} };
|
||||
if (!STATE.color.palette) STATE.color.palette = {};
|
||||
STATE.color.palette[role] = inp.value;
|
||||
paintSwatch(sw, inp.value);
|
||||
// Reflect a valid override straight onto the selected card so the
|
||||
// user sees the change in context, not just in the input row.
|
||||
var n = normHex(inp.value);
|
||||
if (n && selectedIdx >= 0 && cardSwatchRefs[selectedIdx] && cardSwatchRefs[selectedIdx][role]) {
|
||||
cardSwatchRefs[selectedIdx][role].style.background = n;
|
||||
}
|
||||
});
|
||||
hexInputs[role] = inp; wrap.appendChild(inp); row.appendChild(wrap);
|
||||
hexInputs[role] = inp; hexSwatches[role] = sw;
|
||||
line.appendChild(sw); line.appendChild(inp);
|
||||
wrap.appendChild(line); row.appendChild(wrap);
|
||||
});
|
||||
override.appendChild(row);
|
||||
sec.appendChild(override);
|
||||
|
||||
+4
-4
@@ -389,16 +389,16 @@
|
||||
"label": "Path A",
|
||||
"label_zh": "接口后端",
|
||||
"label_en": "Path A",
|
||||
"desc_zh": "触发条件:已配置图像后端;机制:第 5 步运行图像生成脚本。",
|
||||
"desc_en": "IMAGE_BACKEND configured; image_gen.py runs in Step 5."
|
||||
"desc_zh": "强制走接口后端,第 5 步运行 image_gen.py(需已配置 IMAGE_BACKEND)。",
|
||||
"desc_en": "Force the API backend — image_gen.py runs in Step 5 (requires IMAGE_BACKEND)."
|
||||
},
|
||||
{
|
||||
"id": "host-native",
|
||||
"label": "Path B",
|
||||
"label_zh": "宿主原生工具",
|
||||
"label_en": "Path B",
|
||||
"desc_zh": "触发条件:未配置图像后端,且当前环境提供原生图像工具;机制:直接调用宿主工具生成。",
|
||||
"desc_en": "IMAGE_BACKEND not configured and host has a native image tool; host-native generation."
|
||||
"desc_zh": "强制使用当前环境的原生图像工具生成(即使配置了 IMAGE_BACKEND 也跳过接口);需宿主提供原生图像工具(Codex / Claude Code 等)。",
|
||||
"desc_en": "Force the host's native image tool (skips the API even if IMAGE_BACKEND is set); requires a host with a native image tool (Codex / Claude Code / similar)."
|
||||
},
|
||||
{
|
||||
"id": "manual",
|
||||
|
||||
+14
@@ -245,6 +245,7 @@ body {
|
||||
.hex-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.hex-cell { display: flex; flex-direction: column; gap: 4px; }
|
||||
.hex-cell-label { font-size: 11px; color: var(--muted); }
|
||||
.hex-input-line { display: flex; align-items: center; gap: 6px; }
|
||||
.hex-cell input {
|
||||
width: 92px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -253,6 +254,19 @@ body {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
/* Live preview of the value typed into the HEX box. */
|
||||
.hex-swatch {
|
||||
flex: none;
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.hex-swatch.hex-swatch-empty {
|
||||
/* Subtle checker so an empty / invalid value reads as "no color" */
|
||||
background:
|
||||
linear-gradient(45deg, var(--line) 25%, transparent 25%, transparent 75%, var(--line) 75%) 0 0 / 10px 10px,
|
||||
var(--card);
|
||||
}
|
||||
|
||||
/* ---- typography CJK / Latin previews ---- */
|
||||
.font-sample-line { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
|
||||
|
||||
@@ -7,17 +7,19 @@
|
||||
```bash
|
||||
python3 scripts/confirm_ui/server.py <project_path> --daemon --wait
|
||||
python3 scripts/confirm_ui/server.py <project_path> --daemon
|
||||
python3 scripts/confirm_ui/server.py <project_path> --daemon --port 4041
|
||||
python3 scripts/confirm_ui/server.py <project_path> --daemon --port 5051
|
||||
python3 scripts/confirm_ui/server.py <project_path> --no-browser
|
||||
python3 scripts/confirm_ui/server.py <project_path> --timeout 0 # disable idle auto-shutdown
|
||||
python3 scripts/confirm_ui/server.py <project_path> --shutdown # Step 4 cleanup (idempotent)
|
||||
```
|
||||
|
||||
- Binds `127.0.0.1:4040` by default and auto-opens the browser (suppress with `--no-browser`).
|
||||
- Binds `127.0.0.1:5050` by default — or the next free port if another project already holds it (the launch log prints the actual URL) — and auto-opens the browser (suppress with `--no-browser`). `--port <other>` forces a specific port.
|
||||
- **Shares port 5050 with the live preview server** (`svg_editor/server.py`). The two never run at once: confirm is Step 4, live preview is Step 6, and Step 4 always shuts this server down on exit (see `--shutdown`) so the port is free. One port = one forward rule for the whole pipeline. They still keep **separate processes and locks** (`.confirm_ui.lock` vs `.live_preview.lock`).
|
||||
- `--daemon` starts the Flask process in the background; add `--wait` in the main pipeline so the parent command returns only after the page writes a fresh `result.json`. The `--wait` budget defaults to **590 s** (`--wait-timeout`), kept under the typical 600 s tool ceiling — run the launch with a long tool timeout (≈600000 ms). On timeout the parent returns non-zero but the detached server keeps running, so the caller must re-check `result.json` once before the chat fallback (a slow user may confirm just after the wait returns).
|
||||
- Refuses to start unless `<project_path>/confirm_ui/recommendations.json` exists.
|
||||
- `--shutdown` stops a confirm server left running for this project and exits — **idempotent** (a no-op when nothing is running). Tries a graceful `/api/shutdown`, falls back to killing the recorded pid, then clears the lock. SKILL.md Step 4 runs this on every path (page-confirm or chat-fallback) so the page never lingers on the shared port before live preview starts.
|
||||
- Refuses to start unless `<project_path>/confirm_ui/recommendations.json` exists (except `--shutdown`, which needs no recommendations).
|
||||
- Per-project lock at `<project_path>/.confirm_ui.lock` — duplicate launches are refused; stale locks (dead pid) are overwritten.
|
||||
- Idle auto-shutdown after 900 s by default; `/api/shutdown` exits gracefully and releases the lock.
|
||||
- Independent of the live preview server (`svg_editor/server.py`, port 5050) in process, lock, and content model.
|
||||
|
||||
Dependency:
|
||||
|
||||
@@ -37,7 +39,7 @@ pip install flask
|
||||
|
||||
## Catalogs — `static/catalogs.json` (the finite option universe)
|
||||
|
||||
Served by Flask static at `/static/catalogs.json` (the front-end prefers `/api/catalogs` when the server provides it — e.g. canvas sourced live from `config.py CANVAS_FORMATS` for zero drift). Keys: `canvas`, `modes`, `visual_styles` (grouped), `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`. Each entry is `{ "id", "label", "label_zh", "label_en", ... }`; descriptions use `desc_zh` / `desc_en`, and `visual_styles` groups use `group_zh` / `group_en`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must be bilingual. English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Chinese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_en", "items": [...] }]`. `canvas` mirrors `config.py CANVAS_FORMATS` — keep in sync.
|
||||
The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while bilingual labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`. Each entry is `{ "id", "label", "label_zh", "label_en", ... }`; descriptions use `desc_zh` / `desc_en`, and `visual_styles` groups use `group_zh` / `group_en`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must be bilingual. English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Chinese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_en", "items": [...] }]`. For `canvas` you only need to maintain the bilingual labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`.
|
||||
|
||||
## Round-trip data contract
|
||||
|
||||
@@ -131,8 +133,8 @@ Both files live under `<project_path>/confirm_ui/`.
|
||||
```
|
||||
|
||||
- Any option field may instead hold a **free-text custom string** (the user picked **Custom**); `color` / `typography` custom entries set `name: "custom"`. Image usage custom values must be concrete prose plans, not the literal string `"custom"`. The AI interprets custom text against the canonical references.
|
||||
- `image_ai_path` and `image_strategy` are omitted from `result.json` unless `image_usage` is `ai` or a custom image plan that may include generated images.
|
||||
- After the user clicks **Confirm**, the page saves `result.json` and shuts the server down (auto-close). In the default `--daemon --wait` flow, the waiting command returns and the AI reads `result.json` immediately; no second chat confirmation is required. Chat confirmation remains the fallback when the page cannot be used.
|
||||
- `image_ai_path` and `image_strategy` are omitted from `result.json` unless `image_usage` is `ai` or a custom image plan that may include generated images. Both are honored downstream as confirmed choices — and the page is only a convenience surface over the **canonical chat channel**: the same choices made in chat are honored identically when no `result.json` exists. `image_ai_path` drives the Step 5 generation path (`image-generator.md` §7 — `host-native` forces the host tool even when `IMAGE_BACKEND` is set); the chosen `image_strategy` candidate is locked verbatim by Strategist h.5 (no re-pick).
|
||||
- After the user clicks **Confirm**, the page saves `result.json` and shuts the server down (auto-close). In the default `--daemon --wait` flow, the waiting command returns and the AI reads `result.json` immediately; no second chat confirmation is required. Chat confirmation remains the fallback when the page cannot be used. Either way, Step 4 ends with a `--shutdown` cleanup so a never-confirmed page cannot keep holding port 5050 ahead of the Step 6 live preview.
|
||||
|
||||
## Scope
|
||||
|
||||
|
||||
@@ -803,11 +803,14 @@ def main() -> None:
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
if command in {"-h", "--help", "help"}:
|
||||
# Help anywhere — top-level or after a subcommand (e.g. `init --help`) —
|
||||
# prints usage and exits; never treat a help flag as a positional argument.
|
||||
if any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]):
|
||||
print_usage()
|
||||
sys.exit(0)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
manager = ProjectManager()
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,7 +9,7 @@ cross-platform process-liveness check and the claim/read/release lock logic so
|
||||
the two servers cannot drift apart.
|
||||
|
||||
Usage:
|
||||
from server_common import process_alive, read_lock, claim_lock, release_lock
|
||||
from server_common import process_alive, read_lock, claim_lock, release_lock, find_free_port
|
||||
|
||||
Dependencies:
|
||||
None (only uses standard library)
|
||||
@@ -17,10 +17,29 @@ Dependencies:
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def find_free_port(preferred: int, host: str = '127.0.0.1', span: int = 50) -> int:
|
||||
"""Return ``preferred`` if it is bindable, else the next free port within
|
||||
``span``. Lets a new project's UI server coexist with another project's
|
||||
server already holding the default port, instead of crashing on bind — each
|
||||
project ends up on its own port serving its own data. Falls back to
|
||||
``preferred`` if the whole span is taken (let the caller's bind surface it).
|
||||
"""
|
||||
for port in range(preferred, preferred + span):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
||||
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
probe.bind((host, port))
|
||||
return port
|
||||
except OSError:
|
||||
continue
|
||||
return preferred
|
||||
|
||||
|
||||
def process_alive(pid: int) -> bool:
|
||||
"""Return True if a process with this pid is reachable.
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ if str(_ROOT_SCRIPTS_DIR) not in sys.path:
|
||||
|
||||
from server_common import ( # noqa: E402
|
||||
claim_lock as _claim_lock,
|
||||
find_free_port as _find_free_port,
|
||||
process_alive as _process_alive,
|
||||
read_lock as _read_lock,
|
||||
release_lock as _release_lock,
|
||||
@@ -424,6 +425,28 @@ def create_app(
|
||||
return jsonify({'error': 'not found'}), 404
|
||||
return send_from_directory(str(assets_dir), filename)
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def serve_bare_asset(filename: str):
|
||||
"""Resolve a template SVG's bare image href (e.g. `href="cover_bg.png"`).
|
||||
|
||||
Mirror templates copy hrefs verbatim, so a bare filename reaches the
|
||||
browser as `/<filename>` (no `../images/` prefix). Resolve it against the
|
||||
project's images/ then assets/. Every real route (`/api/*`, `/images/*`,
|
||||
`/assets/*`, `/static/*`, `/`) is more specific and matches first; this
|
||||
only catches the leftover bare references and 404s otherwise.
|
||||
"""
|
||||
for base in (images_dir, assets_dir):
|
||||
if not base.exists():
|
||||
continue
|
||||
target = (base / filename).resolve()
|
||||
try:
|
||||
target.relative_to(base.resolve())
|
||||
except ValueError:
|
||||
continue
|
||||
if target.exists() and target.is_file():
|
||||
return send_from_directory(str(base), filename)
|
||||
return jsonify({'error': 'not found'}), 404
|
||||
|
||||
@app.route('/api/slides')
|
||||
def get_slides():
|
||||
svg_dir = app.config['SVG_DIR']
|
||||
@@ -834,12 +857,17 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
logger.error('%s is not a directory', svg_output)
|
||||
return 1
|
||||
|
||||
# Pick a free port: another project's preview/confirm server may already
|
||||
# hold the default, so bind the next free one instead of crashing — each
|
||||
# project then serves its own data on its own port (no cross-project mix-up).
|
||||
port = _find_free_port(args.port)
|
||||
|
||||
# Per-project mutual exclusion. The major driver of orphaned servers is
|
||||
# --live mode (which used to disable idle timeout entirely) combined with
|
||||
# silent restarts; refusing duplicate launches catches the accumulation
|
||||
# at its source. Stale locks (dead pid) are overwritten by _claim_lock.
|
||||
lock_file = project_path / LOCK_FILE_NAME
|
||||
existing = _claim_lock(lock_file, args.port)
|
||||
existing = _claim_lock(lock_file, port)
|
||||
if existing:
|
||||
existing_pid = existing.get('pid', '?')
|
||||
existing_port = existing.get('port', '?')
|
||||
@@ -881,7 +909,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
lock_file=lock_file,
|
||||
)
|
||||
|
||||
url = f'http://localhost:{args.port}'
|
||||
url = f'http://localhost:{port}'
|
||||
if not args.no_browser:
|
||||
webbrowser.open(url)
|
||||
|
||||
@@ -891,7 +919,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
logger.info('project: %s', project_path)
|
||||
logger.info('svg_output: %s (%d slides)', svg_output, svg_count)
|
||||
logger.info('idle timeout: %ds (0 = disabled)', idle_timeout)
|
||||
app.run(host='127.0.0.1', port=args.port, debug=False)
|
||||
app.run(host='127.0.0.1', port=port, debug=False)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+23
-10
@@ -6,6 +6,7 @@ import io
|
||||
import math
|
||||
import re
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
@@ -30,6 +31,26 @@ from .drawingml_paths import (
|
||||
)
|
||||
|
||||
|
||||
def _resolve_external_image(svg_dir: Path, href: str) -> Path:
|
||||
"""Resolve a non-data-URI image href to a file on disk.
|
||||
|
||||
Search order: next to the SVG (``svg_output/``), the project root, the
|
||||
project's ``images/`` (the single runtime image pool — template-bundled
|
||||
bitmaps plus AI / web / user images all live here), then ``templates/``
|
||||
(legacy flat-copied template assets). Raises ``FileNotFoundError`` if none
|
||||
of these exist.
|
||||
"""
|
||||
for candidate in (
|
||||
svg_dir / href,
|
||||
svg_dir.parent / href,
|
||||
svg_dir.parent / 'images' / href,
|
||||
svg_dir.parent / 'templates' / href,
|
||||
):
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
raise FileNotFoundError(f'External image not found: {href}')
|
||||
|
||||
|
||||
def _wrap_shape(
|
||||
shape_id: int, name: str,
|
||||
off_x: int, off_y: int,
|
||||
@@ -1686,11 +1707,7 @@ def convert_image(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
|
||||
else:
|
||||
if ctx.svg_dir is None:
|
||||
return None
|
||||
img_path = ctx.svg_dir / href
|
||||
if not img_path.exists():
|
||||
img_path = ctx.svg_dir.parent / href
|
||||
if not img_path.exists():
|
||||
raise FileNotFoundError(f'External image not found: {href}')
|
||||
img_path = _resolve_external_image(ctx.svg_dir, href)
|
||||
img_format = img_path.suffix.lstrip('.').lower()
|
||||
if img_format == 'jpeg':
|
||||
img_format = 'jpg'
|
||||
@@ -1887,11 +1904,7 @@ def convert_nested_svg(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | N
|
||||
else:
|
||||
if ctx.svg_dir is None:
|
||||
return None
|
||||
img_path = ctx.svg_dir / href
|
||||
if not img_path.exists():
|
||||
img_path = ctx.svg_dir.parent / href
|
||||
if not img_path.exists():
|
||||
raise FileNotFoundError(f'External image not found: {href}')
|
||||
img_path = _resolve_external_image(ctx.svg_dir, href)
|
||||
img_format = img_path.suffix.lstrip('.').lower()
|
||||
if img_format == 'jpeg':
|
||||
img_format = 'jpg'
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
- P03: 02a_chapter
|
||||
- P04: 03a_content_abstract
|
||||
|
||||
> One entry per page **that uses a template SVG**. Key: `P<NN>` matching §IX. Value: the template's SVG basename without extension (e.g., `01_cover`, `03a_content_image_text`) — Executor resolves it to `templates/<chosen_template>/<value>.svg`. Modern templates ship many content-page variants (`03a_content_abstract`, `03b_content_image_text`, `03c_content_three_items` …); the page-type → single-file mapping in `executor-base.md §1` no longer covers them, so this section is the per-page truth.
|
||||
> One entry per page **that uses a template SVG**. Key: `P<NN>` matching §IX. Value: the template's SVG basename without extension (e.g., `01_cover`, `03a_content_image_text`) — Executor resolves it to `templates/<value>.svg`. Modern templates ship many content-page variants (`03a_content_abstract`, `03b_content_image_text`, `03c_content_three_items` …); the page-type → single-file mapping in `executor-base.md §1` no longer covers them, so this section is the per-page truth.
|
||||
>
|
||||
> **No entry for a page** → that page is free design (no template inheritance). Mixed decks are supported: e.g., cover/chapter pages inherit a template while content pages are free.
|
||||
>
|
||||
@@ -132,7 +132,7 @@
|
||||
>
|
||||
> **Whole section omitted** → entire deck is free design. Equivalent to no rows but cleaner; do this when zero pages reference a template.
|
||||
>
|
||||
> **Strategist source**: copy the per-page SVG choices from `design_spec.md §VI Page Roster` (or §IX outline if Roster is absent). Names must match files in `templates/<chosen_template>/` exactly — typos cause silent fallback to free design.
|
||||
> **Strategist source**: copy the per-page SVG choices from `design_spec.md §VI Page Roster` (or §IX outline if Roster is absent). Names must match files in `templates/` exactly — typos cause silent fallback to free design.
|
||||
|
||||
## page_charts
|
||||
- P05: bar_chart
|
||||
|
||||
@@ -41,7 +41,7 @@ The server binds `127.0.0.1:5050`, opens the browser on a local desktop, and edi
|
||||
- **Annotate** (changes that need AI judgement / re-layout): select an element → write the instruction → click **Add annotation** to stage it → click **Apply changes** to write annotation markers → return to the chat and say `apply my annotations` (or quote the browser prompt)
|
||||
- to skip the editor, just describe the change in chat
|
||||
|
||||
Do not wait for confirmation before launching — the user already asked for preview, so launching is the response. Port conflicts → `--port <other>` and report the new URL. Remote access → see the appendix.
|
||||
Do not wait for confirmation before launching — the user already asked for preview, so launching is the response. If another project already holds the port, the launcher auto-advances to the next free one — report the actual URL from the launch log (`--port <other>` still forces a specific port). Remote access → see the appendix.
|
||||
|
||||
---
|
||||
|
||||
@@ -82,7 +82,7 @@ Triggered by the user signals listed in "When to Run".
|
||||
- **Unsaved-work guard**: staged direct edits and annotation changes (added or removed) live in server memory until **Apply changes**; closing the tab triggers the browser's native "leave site?" prompt while any are unapplied, since an idle timeout or process kill would drop them.
|
||||
- **Re-export is chat-driven**: applying changes updates `svg_output/` only. Refreshing the PPTX (finalize + svg_to_pptx) stays a chat step — the editor never runs the export pipeline.
|
||||
- **Stop conditions**: the service stops when the user clicks **Exit preview** in the browser, asks in chat to stop it, the idle timeout fires, or the process is killed externally.
|
||||
- **Port**: default `5050`; override with `--port <other>`.
|
||||
- **Port**: default `5050`, auto-advancing to the next free port when another project already holds it (report the actual URL from the launch log); force a specific port with `--port <other>`.
|
||||
- **Idle timeout**: plain mode `900s`, `--live` mode `7200s`; override with `--timeout <seconds>` (`0` disables).
|
||||
- **Single instance per project**: `<project_path>/.live_preview.lock` records the running pid + port. A second launch against the same project refuses to start and prints the existing URL; stale locks (dead pid) are overwritten on the next launch. Delete the file by hand only if the process is gone but the lock remains (rare — `kill -9` is the only common cause).
|
||||
- **Transient ids**: each element gets a temporary `_edit_N` id while the editor is running. On save, only annotated elements keep their id; unannotated `_edit_N` ids are stripped before write-back.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "shadcn",
|
||||
"repo": "https://github.com/shadcn-ui/ui.git",
|
||||
"ref": "main",
|
||||
"commit": "9197676b3d09b77f51b17b63e5978db29e498ca4",
|
||||
"commit": "6ec117715f8519326f0ae8c9984b930f313bdb71",
|
||||
"adapter": "claude-skill",
|
||||
"sourcePath": "skills/shadcn",
|
||||
"syncedAt": "2026-06-15T15:59:59Z"
|
||||
"syncedAt": "2026-06-16T16:00:00Z"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
Superpowers is a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.
|
||||
|
||||
|
||||
## We're Hiring!
|
||||
|
||||
We're hiring someone to help out full time with Superpowers community and code work.
|
||||
You can read about the job at https://primeradiant.com/jobs/superpowers-community-engineer/
|
||||
If this sounds like someone you know, definitely send them our way.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Give your agent Superpowers: [Claude Code](#claude-code), [Codex CLI](#codex-cli), [Codex App](#codex-app), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [OpenCode](#opencode), [Cursor](#cursor), [GitHub Copilot CLI](#github-copilot-cli).
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"sourceId": "superpowers",
|
||||
"repo": "https://github.com/obra/superpowers.git",
|
||||
"ref": "main",
|
||||
"commit": "6fd4507659784c351abbd2bc264c7162cfd386dc",
|
||||
"commit": "8cf39006140a743dce31ba4046fceab90cc214e6",
|
||||
"adapter": "codex-plugin",
|
||||
"sourcePath": ".",
|
||||
"syncedAt": "2026-06-10T07:28:13Z"
|
||||
"syncedAt": "2026-06-16T16:00:00Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user