From 1205da144b907b19d9b1e8f998120272567a9acc Mon Sep 17 00:00:00 2001 From: testycool Date: Fri, 9 Oct 2026 23:59:58 +0300 Subject: [PATCH 1/5] fix(guide): make step screenshots readable without making the page heavy The HTML guide put text and images in one 860px column. A full-window screenshot (2320px) showed at a third of its size, so the UI text in it could not be read. Showing images at full window width fixed the size but made the page hard to follow: huge images next to small text. Now: - the page column is 1100px; step text keeps an 860px reading width - each image is indented to start where its step text starts, so it reads as part of that step rather than a separate block - the step number and the first line of text share one 28px height, so the number sits centred on the line instead of a few pixels off - an image shown smaller than its real size can be clicked to open at full size on a dark overlay, which fades and scales in over 0.2s (no animation with prefers-reduced-motion); a click or Esc closes it - step text is regular weight at 1.15rem instead of bold 1.3rem, so the screenshots carry the page Printing and PDF keep the old full-width, unindented images; the overlay is hidden in print. Tested: tests/test_export.py passes (20 passed, 1 skipped). In headless Chromium at 1920px, dark scheme, an 18-step guide: number centre 216.8px vs first text line 216.3px; image left edge equals text left edge (476px); no sideways scrolling; the overlay is at opacity 0.58 after 80ms, 1 when open, and hidden after Esc. Co-Authored-By: Claude Opus 5.5 --- src/ainotate/export_html.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/ainotate/export_html.py b/src/ainotate/export_html.py index 769f399..a518f6b 100644 --- a/src/ainotate/export_html.py +++ b/src/ainotate/export_html.py @@ -15,17 +15,36 @@ @media (prefers-color-scheme:dark){:root{--bg:#1c1c1e;--fg:#f5f5f7;--muted:#98989d;--accent:#2f8cff;--line:#3a3a3c;--shadow:0 2px 6px rgba(0,0,0,.5),0 10px 30px rgba(0,0,0,.5)}} *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--fg);font:17px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased} -main{max-width:860px;margin:0 auto;padding:56px 24px 80px} +main{max-width:1100px;margin:0 auto;padding:56px 24px 80px} h1{font-size:2.1rem;line-height:1.2;letter-spacing:-.02em;margin:0 0 12px} .intro{color:var(--muted);font-size:1.1rem;margin:0 0 40px} section.step{margin:0 0 48px;break-inside:avoid;page-break-inside:avoid} -h2{display:flex;gap:14px;align-items:flex-start;font-size:1.3rem;line-height:1.35;font-weight:600;margin:0 0 16px;letter-spacing:-.01em} -h2 .n{flex:none;min-width:2rem;height:2rem;border-radius:1rem;background:var(--accent);color:#fff;font-size:1rem;font-weight:700;display:inline-flex;align-items:center;justify-content:center;margin-top:.05rem} +h2{display:flex;gap:14px;align-items:flex-start;font-size:1.15rem;line-height:28px;font-weight:400;margin:0 0 16px;letter-spacing:-.01em} +h2 .n{flex:none;min-width:28px;height:28px;border-radius:14px;background:var(--accent);color:#fff;font-size:1rem;font-weight:700;display:inline-flex;align-items:center;justify-content:center} img{display:block;max-width:100%;height:auto;border-radius:12px;box-shadow:var(--shadow);border:1px solid var(--line)} +h1,.intro,h2{max-width:860px} +section.step img{margin-left:42px;max-width:calc(100% - 42px)} +img.zoomable{cursor:zoom-in} +#zoom{position:fixed;inset:0;z-index:10;display:flex;overflow:auto;padding:24px;background:rgba(0,0,0,.94);cursor:zoom-out;opacity:0;visibility:hidden;transition:opacity .2s ease,visibility 0s .2s} +#zoom.open{opacity:1;visibility:visible;transition:opacity .2s ease} +#zoom img{max-width:none;margin:auto;box-shadow:none;transform:scale(.96);transition:transform .2s ease} +#zoom.open img{transform:none} +@media (prefers-reduced-motion:reduce){#zoom,#zoom img{transition:none}} @page{size:A4;margin:16mm} -@media print{body{background:#fff;color:#000;font-size:12pt}main{max-width:none;padding:0}img{box-shadow:none;border:1px solid #ccc;border-radius:6px}section.step{margin-bottom:22pt}h2 .n{-webkit-print-color-adjust:exact;print-color-adjust:exact}} +@media print{#zoom{display:none}body{background:#fff;color:#000;font-size:12pt}main{max-width:none;padding:0}img{box-shadow:none;border:1px solid #ccc;border-radius:6px}section.step{margin-bottom:22pt}section.step img{margin-left:0;max-width:100%}h2 .n{-webkit-print-color-adjust:exact;print-color-adjust:exact}} """ +# Click a step image shown smaller than its real size to see it full size; click or Esc closes. +_ZOOM_JS = ( + "const z=document.getElementById('zoom');" + "const mark=()=>document.querySelectorAll('section.step img').forEach(i=>" + "i.classList.toggle('zoomable',i.naturalWidth>i.clientWidth+4));" + "addEventListener('load',mark);addEventListener('resize',mark);" + "addEventListener('click',e=>{if(e.target.closest('#zoom')){z.classList.remove('open');return}" + "const i=e.target.closest('img.zoomable');if(i){z.firstChild.src=i.src;z.classList.add('open')}});" + "addEventListener('keydown',e=>{if(e.key==='Escape')z.classList.remove('open')});" +) + def _html(title, intro, steps) -> str: parts = [] @@ -41,7 +60,8 @@ def _html(title, intro, steps) -> str: return (f'' f'' f'{html.escape(title or "Guide")}' - f'
{head}{lead}{"".join(parts)}
') + f'
{head}{lead}{"".join(parts)}
' + f'
') def _pdf_playwright(html_path: Path, pdf_path: Path) -> None: From 02147516272652bf2559bbd28a975aa505e7e48e Mon Sep 17 00:00:00 2001 From: testycool Date: Sat, 10 Oct 2026 00:23:15 +0300 Subject: [PATCH 2/5] feat(guide): optional bold summary line for each step A guide step had one text field, so a step was either a terse line or a long paragraph with the action buried in it. A reader skimming the guide could not see what each step does at a glance. A step can now carry a "title": a short summary shown in bold next to the number, with the "text" below it as the explanation and the image under both. Steps without a title render exactly as before. Markdown writes the title in bold as the list item; the Pillow PDF fallback puts it on the first line. Tested: tests/test_export.py passes (21 passed, 1 skipped), including a new test for the HTML and Markdown output. In headless Chromium at 1920px, a nine-step guide with titles has the title, the explanation and the image starting at the same left edge (476px), no sideways scrolling. Co-Authored-By: Claude Opus 5.5 --- src/ainotate/export.py | 12 +++++++++--- src/ainotate/export_html.py | 14 +++++++++++--- tests/test_export.py | 13 +++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/ainotate/export.py b/src/ainotate/export.py index 03cdf2c..88cc1f9 100644 --- a/src/ainotate/export.py +++ b/src/ainotate/export.py @@ -111,8 +111,9 @@ def _norm_steps(steps) -> list[dict]: if not img.is_file(): raise ExportError(f"step {i}: image not found: {img}", f"steps[{i - 1}].image") text = str(s.get("text", "")).strip() - alt = str(s.get("alt") or "").strip() or (text.replace("\n", " ") or f"Step {i}") - out.append({"image": img, "text": text, "alt": alt}) + title = str(s.get("title", "")).strip() + alt = str(s.get("alt") or "").strip() or (title or text.replace("\n", " ") or f"Step {i}") + out.append({"image": img, "title": title, "text": text, "alt": alt}) return out @@ -122,6 +123,10 @@ def _md(title, intro, steps, image_names) -> str: lines += [intro.strip(), ""] for i, (s, name) in enumerate(zip(steps, image_names), 1): text = s["text"].replace("\n", "\n ") or f"Step {i}" + if s.get("title"): + lines += [f"{i}. **{s['title']}**", ""] + ([f" {text}", ""] if s["text"] else []) + lines += [f" ![{s['alt'].replace(']', ')').replace('[', '(')}]({name})", ""] + continue lines += [f"{i}. {text}", "", f" ![{s['alt'].replace(']', ')').replace('[', '(')}]({name})", ""] return "\n".join(lines).rstrip() + "\n" @@ -164,7 +169,8 @@ def _same_width(st: list[dict], width: Optional[int], tmp: Path) -> list[dict]: def guide(steps: list, title: str = "", out_dir=None, formats: Sequence[str] = ("md", "html"), intro: str = "", *, name: str = "", draft: bool = False, width: Optional[int] = None) -> GuideResult: - """Build a step-by-step guide. steps: [{"image": path, "text": "...", "alt": optional}]. + """Build a step-by-step guide. steps: [{"image": path, "text": "...", "title": optional, "alt": optional}]. + title is a short bold summary shown above the text. Writes into out_dir (default: a new dir ` ` in the AInotate output dir, or in a temp dir when `draft`) and returns {format: Path}. md: relative images copied to `images/`; html: one self-contained file; pdf: Playwright print of the HTML (A4) diff --git a/src/ainotate/export_html.py b/src/ainotate/export_html.py index a518f6b..565dc5b 100644 --- a/src/ainotate/export_html.py +++ b/src/ainotate/export_html.py @@ -24,6 +24,8 @@ img{display:block;max-width:100%;height:auto;border-radius:12px;box-shadow:var(--shadow);border:1px solid var(--line)} h1,.intro,h2{max-width:860px} section.step img{margin-left:42px;max-width:calc(100% - 42px)} +h2 .t{font-weight:600} +p.say{max-width:818px;margin:-8px 0 16px 42px;line-height:1.6} img.zoomable{cursor:zoom-in} #zoom{position:fixed;inset:0;z-index:10;display:flex;overflow:auto;padding:24px;background:rgba(0,0,0,.94);cursor:zoom-out;opacity:0;visibility:hidden;transition:opacity .2s ease,visibility 0s .2s} #zoom.open{opacity:1;visibility:visible;transition:opacity .2s ease} @@ -51,9 +53,15 @@ def _html(title, intro, steps) -> str: for i, s in enumerate(steps, 1): mime = mimetypes.guess_type(str(s["image"]))[0] or "image/png" b64 = base64.b64encode(s["image"].read_bytes()).decode("ascii") - text = html.escape(s["text"]).replace("\n", "
") or f"Step {i}" + text = html.escape(s["text"]).replace("\n", "
") + title = html.escape(s.get("title", "")) + if title: + head_html = f'{title}' + body = f'

{text}

' if text else "" + else: + head_html, body = f"{text or f'Step {i}'}", "" parts.append( - f'

{i}{text}

' + f'

{i}{head_html}

{body}' f'{html.escape(s[
') head = f"

{html.escape(title)}

" if title else "" lead = f'

{html.escape(intro).replace(chr(10), "
")}

' if intro else "" @@ -151,7 +159,7 @@ def text(lines, font, step, x, fill): d.text((M + r, y0 + r), str(i), font=style.load_font(26), fill="white", anchor="mm") tf = style.load_font(36) st["y"] = y0 + 4 - text(_wrap(d, s["text"] or f"Step {i}", tf, W - 2 * M - 2 * r - 20), tf, 46, M + 2 * r + 20, + text(_wrap(d, "\n".join(x for x in (s.get("title", ""), s["text"]) if x) or f"Step {i}", tf, W - 2 * M - 2 * r - 20), tf, 46, M + 2 * r + 20, (29, 29, 31)) y = max(st["y"], y0 + 2 * r if st["d"] is d else 0) + 28 im = _rgb(_open(s["image"])) diff --git a/tests/test_export.py b/tests/test_export.py index 2c3da90..f4535fc 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -287,3 +287,16 @@ def save_spy(self, fp, format=None, **kw): assert (tmp_path / "x.pdf").read_bytes()[:5] == b"%PDF-" assert len(pasted) == 2 and all(h >= export_html.MIN_IMAGE_H for _, h in pasted) assert pages[0] >= 3 # text spilled over a page, image on its own page, step 2 + + +def test_guide_step_title_shows_bold_summary_above_text(tmp_path): + from PIL import Image + from ainotate.export import guide + img = tmp_path / "a.png" + Image.new("RGB", (40, 30), "white").save(img) + res = guide([{"image": str(img), "title": "Deploy a model", "text": "Pick one and press Deploy."}], + "T", tmp_path / "out", formats=("md", "html")) + h = res["html"].read_text() + assert 'Deploy a model' in h and '

Pick one and press Deploy.

' in h + md = res["md"].read_text() + assert "1. **Deploy a model**" in md and " Pick one and press Deploy." in md From 74fc91cb6f6c03e94fb22a0fe7774f73aacb1ec1 Mon Sep 17 00:00:00 2001 From: testycool Date: Sat, 10 Oct 2026 00:29:15 +0300 Subject: [PATCH 3/5] feat(guide): bold, code and sublists in step explanations Step text was plain, so a guide could not show a button name in bold, a field or command as code, or a short list of options under a step. Those had to be crammed into one sentence. Step text now takes a small Markdown subset: **bold**, `code`, and lines starting with "- " as a list. In HTML they render inside the step, indented under its number; Markdown keeps them as written, with a blank line after a list so the next sentence does not join its last item; the Pillow PDF fallback drops the marks and draws list lines as bullets. A step without a title keeps its text in the heading, with bold and code but no list. Tested: tests/test_export.py passes (22 passed, 1 skipped), with a new test for all three marks in HTML, Markdown and PDF. The full suite has one failure, tests/test_privacy.py::test_dom_fixture, which also fails on the commit before this one. Rendered a demo step in headless Chromium, dark scheme: code text is 15.3px. Co-Authored-By: Claude Opus 5.5 --- src/ainotate/export.py | 14 ++++++++- src/ainotate/export_html.py | 59 +++++++++++++++++++++++++++++++++---- tests/test_export.py | 18 ++++++++++- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/ainotate/export.py b/src/ainotate/export.py index 88cc1f9..3b0c703 100644 --- a/src/ainotate/export.py +++ b/src/ainotate/export.py @@ -117,12 +117,24 @@ def _norm_steps(steps) -> list[dict]: return out +def _md_text(raw: str) -> str: + """Blank line after a "- " list so the next sentence is not read as part of its last item.""" + out, prev = [], False + for line in raw.split("\n"): + item = line.strip().startswith(("- ", "* ")) + if prev and not item and line.strip(): + out.append("") + out.append(line) + prev = item + return "\n".join(out) + + def _md(title, intro, steps, image_names) -> str: lines = [f"# {title}", ""] if title else [] if intro: lines += [intro.strip(), ""] for i, (s, name) in enumerate(zip(steps, image_names), 1): - text = s["text"].replace("\n", "\n ") or f"Step {i}" + text = _md_text(s["text"]).replace("\n", "\n ").replace("\n \n", "\n\n") or f"Step {i}" if s.get("title"): lines += [f"{i}. **{s['title']}**", ""] + ([f" {text}", ""] if s["text"] else []) lines += [f" ![{s['alt'].replace(']', ')').replace('[', '(')}]({name})", ""] diff --git a/src/ainotate/export_html.py b/src/ainotate/export_html.py index 565dc5b..0eb03af 100644 --- a/src/ainotate/export_html.py +++ b/src/ainotate/export_html.py @@ -4,6 +4,7 @@ import base64 import html import mimetypes +import re from pathlib import Path from PIL import Image, ImageDraw @@ -25,7 +26,11 @@ h1,.intro,h2{max-width:860px} section.step img{margin-left:42px;max-width:calc(100% - 42px)} h2 .t{font-weight:600} -p.say{max-width:818px;margin:-8px 0 16px 42px;line-height:1.6} +.say{max-width:818px;margin:-8px 0 16px 42px;line-height:1.6} +.say p{margin:0 0 8px} +.say ul{margin:0 0 8px;padding-left:22px} +.say li{margin:2px 0} +code{font:.9em/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:var(--line);padding:1px 5px;border-radius:4px} img.zoomable{cursor:zoom-in} #zoom{position:fixed;inset:0;z-index:10;display:flex;overflow:auto;padding:24px;background:rgba(0,0,0,.94);cursor:zoom-out;opacity:0;visibility:hidden;transition:opacity .2s ease,visibility 0s .2s} #zoom.open{opacity:1;visibility:visible;transition:opacity .2s ease} @@ -48,17 +53,61 @@ ) +def _inline(raw: str) -> str: + """Escape, then **bold** and `code`.""" + parts = re.split(r"(`[^`\n]+`)", raw) + out = [] + for k, part in enumerate(parts): + if k % 2: + out.append(f"{html.escape(part[1:-1])}") + else: + out.append(re.sub(r"\*\*(.+?)\*\*", r"\1", html.escape(part))) + return "".join(out) + + +def _rich(raw: str) -> str: + """Step explanation: inline marks, and lines starting with "- " become a list.""" + blocks, para, items = [], [], [] + def flush(): + if para: + blocks.append(f"

{'
'.join(para)}

") + para.clear() + if items: + blocks.append(f"
    {''.join(f'
  • {i}
  • ' for i in items)}
") + items.clear() + for line in raw.split("\n"): + s = line.strip() + if s.startswith(("- ", "* ")): + if para: + flush() + items.append(_inline(s[2:])) + elif not s: + flush() + else: + if items: + flush() + para.append(_inline(s)) + flush() + return "".join(blocks) + + +def _plain(raw: str) -> str: + """Text for the Pillow PDF: drop the marks, bullets for list lines.""" + raw = re.sub(r"\*\*(.+?)\*\*", r"\1", raw).replace("`", "") + return re.sub(r"(?m)^\s*[-*] ", "• ", raw) + + def _html(title, intro, steps) -> str: parts = [] for i, s in enumerate(steps, 1): mime = mimetypes.guess_type(str(s["image"]))[0] or "image/png" b64 = base64.b64encode(s["image"].read_bytes()).decode("ascii") - text = html.escape(s["text"]).replace("\n", "
") - title = html.escape(s.get("title", "")) + title = _inline(s.get("title", "")) if title: head_html = f'{title}' - body = f'

{text}

' if text else "" + body = f'
{_rich(s["text"])}
' if s["text"] else "" else: + text = "
".join(_inline(ln) for ln in s["text"].split("\n")) head_html, body = f"{text or f'Step {i}'}", "" parts.append( f'

{i}{head_html}

{body}' @@ -159,7 +208,7 @@ def text(lines, font, step, x, fill): d.text((M + r, y0 + r), str(i), font=style.load_font(26), fill="white", anchor="mm") tf = style.load_font(36) st["y"] = y0 + 4 - text(_wrap(d, "\n".join(x for x in (s.get("title", ""), s["text"]) if x) or f"Step {i}", tf, W - 2 * M - 2 * r - 20), tf, 46, M + 2 * r + 20, + text(_wrap(d, _plain("\n".join(x for x in (s.get("title", ""), s["text"]) if x)) or f"Step {i}", tf, W - 2 * M - 2 * r - 20), tf, 46, M + 2 * r + 20, (29, 29, 31)) y = max(st["y"], y0 + 2 * r if st["d"] is d else 0) + 28 im = _rgb(_open(s["image"])) diff --git a/tests/test_export.py b/tests/test_export.py index f4535fc..6dea5ee 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -297,6 +297,22 @@ def test_guide_step_title_shows_bold_summary_above_text(tmp_path): res = guide([{"image": str(img), "title": "Deploy a model", "text": "Pick one and press Deploy."}], "T", tmp_path / "out", formats=("md", "html")) h = res["html"].read_text() - assert 'Deploy a model' in h and '

Pick one and press Deploy.

' in h + assert 'Deploy a model' in h and '

Pick one and press Deploy.

' in h md = res["md"].read_text() assert "1. **Deploy a model**" in md and " Pick one and press Deploy." in md + + +def test_guide_step_text_supports_bold_code_and_sublists(tmp_path): + from PIL import Image + from ainotate.export import guide + img = tmp_path / "a.png" + Image.new("RGB", (40, 30), "white").save(img) + text = "Run `ainotate doctor` first.\n- **Mac:** use brew\n- Linux: \nDone." + res = guide([{"image": str(img), "title": "Install", "text": text}], "T", tmp_path / "out", + formats=("md", "html", "pdf")) + h = res["html"].read_text() + assert "ainotate doctor" in h + assert "
  • Mac: use brew
  • Linux: <apt>
" in h + md = res["md"].read_text() + assert " - **Mac:** use brew" in md and "<apt>

Done.

" in h and "apt>\n\n Done." in md + assert res["pdf"].stat().st_size > 0 From 4f90ee5add88eafe9f2ec0c6813e1181adf8fa20 Mon Sep 17 00:00:00 2001 From: testycool Date: Sat, 10 Oct 2026 00:31:38 +0300 Subject: [PATCH 4/5] fix(guide): fit the zoomed screenshot to the window, over a lighter overlay The zoom opened the image at its real pixel size, so a wide screenshot needed sideways scrolling to read, and the near-black overlay hid the guide completely, so it felt like leaving the page. The zoomed image now scales down to fit the window in both directions, and the overlay is 70% black, so the guide stays faintly visible behind. Tested: tests/test_export.py passes. In headless Chromium a 2320px screenshot opens at 1872x647 in a 1920x1080 window and 1318x456 in 1366x768, with no scrolling in either direction. Co-Authored-By: Claude Opus 5.5 --- src/ainotate/export_html.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ainotate/export_html.py b/src/ainotate/export_html.py index 0eb03af..3dc5a36 100644 --- a/src/ainotate/export_html.py +++ b/src/ainotate/export_html.py @@ -32,16 +32,16 @@ .say li{margin:2px 0} code{font:.9em/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:var(--line);padding:1px 5px;border-radius:4px} img.zoomable{cursor:zoom-in} -#zoom{position:fixed;inset:0;z-index:10;display:flex;overflow:auto;padding:24px;background:rgba(0,0,0,.94);cursor:zoom-out;opacity:0;visibility:hidden;transition:opacity .2s ease,visibility 0s .2s} +#zoom{position:fixed;inset:0;z-index:10;display:flex;overflow:hidden;padding:24px;background:rgba(0,0,0,.7);cursor:zoom-out;opacity:0;visibility:hidden;transition:opacity .2s ease,visibility 0s .2s} #zoom.open{opacity:1;visibility:visible;transition:opacity .2s ease} -#zoom img{max-width:none;margin:auto;box-shadow:none;transform:scale(.96);transition:transform .2s ease} +#zoom img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;margin:auto;box-shadow:0 10px 40px rgba(0,0,0,.5);transform:scale(.96);transition:transform .2s ease} #zoom.open img{transform:none} @media (prefers-reduced-motion:reduce){#zoom,#zoom img{transition:none}} @page{size:A4;margin:16mm} @media print{#zoom{display:none}body{background:#fff;color:#000;font-size:12pt}main{max-width:none;padding:0}img{box-shadow:none;border:1px solid #ccc;border-radius:6px}section.step{margin-bottom:22pt}section.step img{margin-left:0;max-width:100%}h2 .n{-webkit-print-color-adjust:exact;print-color-adjust:exact}} """ -# Click a step image shown smaller than its real size to see it full size; click or Esc closes. +# Click a step image shown smaller than its real size to see it as large as the window allows; click or Esc closes. _ZOOM_JS = ( "const z=document.getElementById('zoom');" "const mark=()=>document.querySelectorAll('section.step img').forEach(i=>" From 01b706da9b1085ea4a15ede0dc8719d90c6e59ba Mon Sep 17 00:00:00 2001 From: testycool Date: Sat, 10 Oct 2026 00:31:58 +0300 Subject: [PATCH 5/5] docs(skill): tell agents about step titles and formatting in guides Agents write steps.json from this reference, so without it they would never use the bold summary or the bold, code and list formatting that guide steps now support. Co-Authored-By: Claude Opus 5.5 --- skills/ainotate/references/spec.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skills/ainotate/references/spec.md b/skills/ainotate/references/spec.md index e021477..4aaf10c 100644 --- a/skills/ainotate/references/spec.md +++ b/skills/ainotate/references/spec.md @@ -142,6 +142,8 @@ the target rects and label boxes without saving). Every command has `--help` wit Steps file for `guide`: `[{"image": "1.png", "text": "Open Settings", "alt": "..."}]` or `{"title": "...", "intro": "...", "steps": [...]}`; relative image paths resolve next to it. +A step's optional `"title"` is a short bold summary next to its number; `"text"` then +explains it below, and can use `**bold**`, `` `code` `` and lines starting with `- ` as a list. ## Output