octocode-cli 1.3.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +193 -34
  2. package/out/chunks/chunk-7476PETK.js +309 -0
  3. package/out/chunks/chunk-CVNNNSMQ.js +26 -0
  4. package/out/chunks/chunk-OQBJTZWK.js +60 -0
  5. package/out/chunks/chunk-UCZCF3BQ.js +9 -0
  6. package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
  7. package/out/chunks/commands-XBFPLHSQ.js +8 -0
  8. package/out/chunks/help-P7TCOYAJ.js +10 -0
  9. package/out/chunks/main-help-ULF5PAQY.js +10 -0
  10. package/out/chunks/prompts-5E6VKRX5.js +8 -0
  11. package/out/chunks/spinner-URV2OX6O.js +8 -0
  12. package/out/chunks/tool-command-M6VA7P2F.js +8 -0
  13. package/out/octocode-cli.js +1 -1
  14. package/package.json +5 -3
  15. package/skills/README.md +13 -1
  16. package/skills/agentic-flow-best-practices/SKILL.md +280 -0
  17. package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
  18. package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
  19. package/skills/agentic-flow-best-practices/references/resources.md +112 -0
  20. package/skills/octocode-chrome-devtools/README.md +541 -0
  21. package/skills/octocode-chrome-devtools/SKILL.md +197 -0
  22. package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
  23. package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
  24. package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
  25. package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
  26. package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
  27. package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
  28. package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
  29. package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
  30. package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
  31. package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
  32. package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
  33. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
  34. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
  35. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
  36. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
  37. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
  38. package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
  39. package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
  40. package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
  41. package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
  42. package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
  43. package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
  44. package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
  45. package/skills/octocode-design/README.md +2 -2
  46. package/skills/octocode-documentation-writer/README.md +1 -1
  47. package/skills/octocode-engineer/README.md +1 -1
  48. package/skills/octocode-engineer/SKILL.md +4 -2
  49. package/skills/octocode-engineer/references/output-files.md +3 -3
  50. package/skills/octocode-engineer/scripts/run.js +136 -136
  51. package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
  52. package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
  53. package/skills/octocode-install/SKILL.md +1 -1
  54. package/skills/octocode-pull-request-reviewer/README.md +5 -5
  55. package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
  56. package/skills/octocode-research/AGENTS.md +1 -1
  57. package/skills/octocode-research/README.md +2 -2
  58. package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
  59. package/skills/octocode-research/scripts/server.js +191 -246
  60. package/skills/octocode-research/src/routes/github.ts +4 -21
  61. package/skills/octocode-research/src/routes/local.ts +4 -21
  62. package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
  63. package/skills/octocode-search-skill/SKILL.md +245 -229
  64. package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
  65. package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
  66. package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
  67. package/skills/octocode-search-skill/references/install-reference.md +130 -0
  68. package/skills/octocode-search-skill/references/references-template.md +27 -0
  69. package/skills/octocode-search-skill/references/references.md +62 -0
  70. package/skills/octocode-slides/README.md +307 -0
  71. package/skills/octocode-slides/SKILL.md +410 -0
  72. package/skills/octocode-slides/references/01-brief.md +156 -0
  73. package/skills/octocode-slides/references/02-research.md +149 -0
  74. package/skills/octocode-slides/references/03-outline.md +172 -0
  75. package/skills/octocode-slides/references/04-design.md +301 -0
  76. package/skills/octocode-slides/references/05-implementation.md +213 -0
  77. package/skills/octocode-slides/references/06-review.md +258 -0
  78. package/skills/octocode-slides/references/animation.md +281 -0
  79. package/skills/octocode-slides/references/design-system.md +316 -0
  80. package/skills/octocode-slides/references/html-templates.md +673 -0
  81. package/skills/octocode-slides/references/image-generation.md +448 -0
  82. package/skills/octocode-slides/references/resources.md +840 -0
  83. package/skills/octocode-slides/references/slide-rules.md +541 -0
  84. package/skills/octocode-slides/references/wireframes.md +727 -0
  85. package/skills/octocode-slides/scripts/animation.js +182 -0
  86. package/skills/octocode-slides/scripts/base.css +353 -0
  87. package/skills/octocode-slides/scripts/base.html +655 -0
  88. package/skills/octocode-slides/scripts/generate_image.py +221 -0
  89. package/skills/octocode-slides/scripts/navbridge.js +79 -0
  90. package/skills/octocode-slides/scripts/presenter.js +316 -0
  91. package/skills/octocode-slides/scripts/slide.html +248 -0
  92. package/skills/octocode-stats/SKILL.md +73 -0
  93. package/skills/octocode-stats/assets/template.html +1332 -0
  94. package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
  95. package/assets/example.png +0 -0
  96. package/out/chunks/chunk-QCY7Q7YW.js +0 -389
  97. package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
  98. package/out/chunks/commands-OCTZP2TO.js +0 -51
  99. package/out/chunks/help-XPXP46ZT.js +0 -10
  100. package/out/chunks/main-help-35HX2UDQ.js +0 -10
  101. package/out/chunks/tool-command-HOSMVLNK.js +0 -8
  102. package/skills/octocode-search-skill/INSTALL_REFERENCE.md +0 -112
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # /// script
4
+ # requires-python = ">=3.11"
5
+ # dependencies = [
6
+ # "google-genai",
7
+ # "pillow",
8
+ # ]
9
+ # ///
10
+
11
+ """
12
+ Generate and edit images using Nano Banana 2 (Gemini 3.1 Flash Image Preview).
13
+
14
+ Usage examples:
15
+ uv run skills/octocode-slides/scripts/generate_image.py \
16
+ --prompt "A serene Japanese garden at sunrise" \
17
+ --filename "assets/hero-2026-05-10-22-00-00.png" \
18
+ --resolution 2K \
19
+ --aspect-ratio 16:9
20
+
21
+ uv run skills/octocode-slides/scripts/generate_image.py \
22
+ --prompt "Turn this into a cinematic poster" \
23
+ --filename "assets/edited.png" \
24
+ --input-image "assets/reference.png" \
25
+ --resolution 4K
26
+
27
+ API key resolution order:
28
+ 1. --api-key argument
29
+ 2. GEMINI_API_KEY environment variable
30
+
31
+ Get your key: https://aistudio.google.com/apikey
32
+ """
33
+
34
+ import argparse
35
+ import os
36
+ import sys
37
+ from io import BytesIO
38
+ from pathlib import Path
39
+
40
+ SUPPORTED_ASPECT_RATIOS = [
41
+ "1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1",
42
+ "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9",
43
+ ]
44
+ SUPPORTED_RESOLUTIONS = ["512px", "1K", "2K", "4K"]
45
+ MAX_REFERENCE_IMAGES = 14
46
+
47
+
48
+ def get_api_key(provided_key: str | None) -> str | None:
49
+ """Get API key from argument first, then environment."""
50
+ if provided_key:
51
+ return provided_key
52
+ return os.environ.get("GEMINI_API_KEY")
53
+
54
+
55
+ def save_png(image, output_path: Path) -> None:
56
+ """Save image as PNG in RGB mode."""
57
+ if image.mode == "RGBA":
58
+ image.convert("RGB").save(str(output_path), "PNG")
59
+ elif image.mode == "RGB":
60
+ image.save(str(output_path), "PNG")
61
+ else:
62
+ image.convert("RGB").save(str(output_path), "PNG")
63
+
64
+
65
+ def auto_detect_resolution(input_images, user_resolution: str) -> str:
66
+ """Auto-select output size from input references when user left default 1K."""
67
+ if user_resolution != "1K" or not input_images:
68
+ return user_resolution
69
+
70
+ max_dim = max(max(image.size) for image in input_images)
71
+ if max_dim >= 3000:
72
+ return "4K"
73
+ if max_dim >= 1500:
74
+ return "2K"
75
+ if max_dim <= 640:
76
+ return "512px"
77
+ return "1K"
78
+
79
+
80
+ def main() -> None:
81
+ parser = argparse.ArgumentParser(
82
+ description="Generate/edit images with Nano Banana 2 (gemini-3.1-flash-image-preview)"
83
+ )
84
+ parser.add_argument("--prompt", "-p", required=True, help="Image prompt/instructions")
85
+ parser.add_argument("--filename", "-f", required=True, help="Output filename (.png recommended)")
86
+ parser.add_argument(
87
+ "--input-image",
88
+ "-i",
89
+ action="append",
90
+ default=[],
91
+ help=(
92
+ "Optional reference/input image path. Repeat flag for multiple images "
93
+ f"(up to {MAX_REFERENCE_IMAGES})."
94
+ ),
95
+ )
96
+ parser.add_argument(
97
+ "--resolution",
98
+ "-r",
99
+ choices=SUPPORTED_RESOLUTIONS,
100
+ default="1K",
101
+ help="Output resolution: 512px, 1K (default), 2K, or 4K",
102
+ )
103
+ parser.add_argument(
104
+ "--aspect-ratio",
105
+ "-a",
106
+ choices=SUPPORTED_ASPECT_RATIOS,
107
+ help="Optional output aspect ratio",
108
+ )
109
+ parser.add_argument("--api-key", "-k", help="Gemini API key (overrides GEMINI_API_KEY env var)")
110
+
111
+ args = parser.parse_args()
112
+
113
+ api_key = get_api_key(args.api_key)
114
+ if not api_key:
115
+ print("Error: No API key provided.", file=sys.stderr)
116
+ print(" 1. Pass --api-key <your-key>", file=sys.stderr)
117
+ print(" 2. Or set: export GEMINI_API_KEY='...'", file=sys.stderr)
118
+ print(" Get a key: https://aistudio.google.com/apikey", file=sys.stderr)
119
+ sys.exit(1)
120
+
121
+ if len(args.input_image) > MAX_REFERENCE_IMAGES:
122
+ print(
123
+ f"Error: Too many input images ({len(args.input_image)}). "
124
+ f"Maximum supported: {MAX_REFERENCE_IMAGES}.",
125
+ file=sys.stderr,
126
+ )
127
+ sys.exit(1)
128
+
129
+ from google import genai
130
+ from google.genai import types
131
+ from PIL import Image as PILImage
132
+
133
+ client = genai.Client(api_key=api_key)
134
+
135
+ output_path = Path(args.filename)
136
+ output_path.parent.mkdir(parents=True, exist_ok=True)
137
+
138
+ input_images = []
139
+ if args.input_image:
140
+ for image_path in args.input_image:
141
+ try:
142
+ image = PILImage.open(image_path)
143
+ input_images.append(image)
144
+ print(f"Loaded input image: {image_path}")
145
+ except Exception as exc:
146
+ print(f"Error loading input image '{image_path}': {exc}", file=sys.stderr)
147
+ sys.exit(1)
148
+
149
+ output_resolution = auto_detect_resolution(input_images, args.resolution)
150
+
151
+ image_config_kwargs = {"image_size": output_resolution}
152
+ if args.aspect_ratio:
153
+ image_config_kwargs["aspect_ratio"] = args.aspect_ratio
154
+
155
+ if input_images:
156
+ contents = [args.prompt, *input_images]
157
+ print(
158
+ f"Editing/generating from {len(input_images)} reference image(s), "
159
+ f"resolution {output_resolution}"
160
+ + (f", aspect ratio {args.aspect_ratio}" if args.aspect_ratio else "")
161
+ + "..."
162
+ )
163
+ else:
164
+ contents = [args.prompt]
165
+ print(
166
+ f"Generating image, resolution {output_resolution}"
167
+ + (f", aspect ratio {args.aspect_ratio}" if args.aspect_ratio else "")
168
+ + "..."
169
+ )
170
+
171
+ try:
172
+ response = client.models.generate_content(
173
+ model="gemini-3.1-flash-image-preview",
174
+ contents=contents,
175
+ config=types.GenerateContentConfig(
176
+ response_modalities=["TEXT", "IMAGE"],
177
+ image_config=types.ImageConfig(**image_config_kwargs),
178
+ ),
179
+ )
180
+
181
+ image_parts = 0
182
+ saved_paths = []
183
+
184
+ for part in response.parts:
185
+ if part.text is not None:
186
+ print(f"Model response: {part.text}")
187
+ elif part.inline_data is not None:
188
+ image_parts += 1
189
+
190
+ image_data = part.inline_data.data
191
+ if isinstance(image_data, str):
192
+ import base64
193
+ image_data = base64.b64decode(image_data)
194
+
195
+ image = PILImage.open(BytesIO(image_data))
196
+
197
+ if image_parts == 1:
198
+ current_path = output_path
199
+ else:
200
+ current_path = output_path.with_name(
201
+ f"{output_path.stem}-{image_parts}{output_path.suffix or '.png'}"
202
+ )
203
+
204
+ save_png(image, current_path)
205
+ saved_paths.append(current_path.resolve())
206
+
207
+ if saved_paths:
208
+ print("\nSaved:")
209
+ for path in saved_paths:
210
+ print(f" {path}")
211
+ else:
212
+ print("Error: No image was generated in the response.", file=sys.stderr)
213
+ sys.exit(1)
214
+
215
+ except Exception as exc:
216
+ print(f"Error generating image: {exc}", file=sys.stderr)
217
+ sys.exit(1)
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Slide → parent navigation bridge.
3
+ *
4
+ * When a slide iframe has keyboard focus (after the user clicks anywhere
5
+ * inside it), arrow keys fire on the iframe's document, not the parent
6
+ * window. This script forwards those keys to the parent via postMessage
7
+ * so the navigation controller in index.html can keep working.
8
+ *
9
+ * Usage: copy to js/navbridge.js at the deck root.
10
+ * Each slide includes: <script src="../js/navbridge.js"></script>
11
+ */
12
+ (function () {
13
+ if (window.parent === window) return; // standalone, not embedded
14
+
15
+ var NAV_KEYS = {
16
+ ArrowLeft: 1, ArrowRight: 1, ArrowUp: 1, ArrowDown: 1,
17
+ PageUp: 1, PageDown: 1, Home: 1, End: 1,
18
+ ' ': 1, g: 1, G: 1, f: 1, F: 1,
19
+ b: 1, B: 1, w: 1, W: 1, p: 1, P: 1
20
+ };
21
+
22
+ function isTypingTarget(el) {
23
+ if (!el) return false;
24
+ var tag = (el.tagName || '').toLowerCase();
25
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
26
+ return !!el.isContentEditable;
27
+ }
28
+
29
+ function hasTextSelection() {
30
+ try {
31
+ var sel = window.getSelection && window.getSelection();
32
+ return !!(sel && String(sel).length > 0);
33
+ } catch (_) { return false; }
34
+ }
35
+
36
+ function send(key) {
37
+ try {
38
+ window.parent.postMessage(
39
+ { type: 'octocode-slides:nav', key: key },
40
+ '*'
41
+ );
42
+ } catch (_) { /* ignore cross-origin failures */ }
43
+ }
44
+
45
+ document.addEventListener('keydown', function (e) {
46
+ if (isTypingTarget(e.target)) return;
47
+ if (!NAV_KEYS[e.key]) return;
48
+ if (e.key === ' ' && hasTextSelection()) return;
49
+ send(e.key);
50
+ e.preventDefault();
51
+ }, true);
52
+
53
+ // Forward mouse activity so the parent HUD wakes back up
54
+ document.addEventListener('mousemove', function () {
55
+ try { window.parent.postMessage({ type: 'octocode-slides:activity' }, '*'); }
56
+ catch (_) {}
57
+ }, { passive: true });
58
+
59
+ // ── Step-animation back-channel ─────────────────────────────────────────
60
+ // The parent forwards nav keys here (instead of navigating immediately) so
61
+ // animation.js gets first refusal on each keystroke, even when the parent
62
+ // window has focus. We dispatch a synthetic keydown on the slide's document:
63
+ // • animation.js (capture, registered first) intercepts — if a step is
64
+ // pending it calls stopImmediatePropagation, navbridge's own listener
65
+ // never fires, no postMessage goes back, parent stays on this slide.
66
+ // • If animation.js has nothing left to consume the event falls through to
67
+ // navbridge's keydown listener, which posts back to the parent, and the
68
+ // parent's message handler calls handleKey(passthrough:true) → navigate.
69
+ // Slides without animation.js: the synthetic event reaches navbridge's
70
+ // listener directly and posts back immediately — no delay, no hang.
71
+ window.addEventListener('message', function (event) {
72
+ var data = event.data;
73
+ if (!data || data.type !== 'octocode-slides:key' || !data.key) return;
74
+ if (!NAV_KEYS[data.key]) return;
75
+ document.dispatchEvent(
76
+ new KeyboardEvent('keydown', { key: data.key, bubbles: true, cancelable: true })
77
+ );
78
+ });
79
+ })();
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Octocode Slides — Presenter View
3
+ *
4
+ * Press P in the generated deck shell to open the presenter popup.
5
+ *
6
+ * The popup shows:
7
+ * - Current slide live preview (scaled iframe)
8
+ * - Next slide live preview (scaled iframe)
9
+ * - Speaker notes
10
+ * - Running timer (persisted across page refreshes via localStorage)
11
+ * - Jump-to-slide control (updates the main deck window)
12
+ *
13
+ * Requires same-origin serving (npx serve works; file:// does not).
14
+ * The default scripts/base.html template already loads and wires this file;
15
+ * custom index.html files should copy that integration instead of inventing
16
+ * a second keyboard handler.
17
+ *
18
+ * Expected call site in index.html:
19
+ *
20
+ * initPresenter(
21
+ * () => stage.querySelector('.slide-frame[data-active]'),
22
+ * playable,
23
+ * () => current
24
+ * );
25
+ */
26
+ function initPresenter(getActiveFrame, playable, getCurrentIndex) {
27
+ // ── localStorage key — scoped to this deck (title + pathname) ───────────
28
+ // Survives page refreshes on localhost so the timer keeps running while
29
+ // you edit slides. Cleared on "Reset timer" or when the deck changes.
30
+ var LS_KEY = 'octocode-slides:timer:' +
31
+ (document.title || 'deck') + ':' + location.pathname;
32
+
33
+ var win = null;
34
+ var elapsed = 0;
35
+ var timerRef = null;
36
+ var startTime = null;
37
+
38
+ // ── Restore timer across page refreshes ─────────────────────────────────
39
+ (function restoreTimer() {
40
+ try {
41
+ var saved = localStorage.getItem(LS_KEY);
42
+ if (!saved) return;
43
+ var data = JSON.parse(saved);
44
+ if (data && data.startTime) {
45
+ startTime = data.startTime;
46
+ elapsed = Math.floor((Date.now() - startTime) / 1000);
47
+ if (elapsed < 0) elapsed = 0;
48
+ }
49
+ } catch (_) {}
50
+ })();
51
+
52
+ // ── Popup HTML ───────────────────────────────────────────────────────────
53
+ // Layout (4-row grid):
54
+ // topbar — slide name (left) + running timer (right)
55
+ // previews — current slide iframe + next slide iframe (two columns)
56
+ // notes — scrollable speaker notes (fills remaining height)
57
+ // bottom — jump-to-slide input · Reset timer · Close
58
+ var POPUP_HTML =
59
+ '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">' +
60
+ '<title>Presenter View</title><style>' +
61
+ '*{box-sizing:border-box;margin:0;padding:0}' +
62
+ 'html,body{height:100%;background:#0a0a0a;color:#e0e0e0;' +
63
+ 'font-family:system-ui,sans-serif;overflow:hidden}' +
64
+ '#wrap{display:grid;grid-template-rows:auto auto 1fr auto;' +
65
+ 'height:100vh;padding:14px 18px;gap:10px}' +
66
+
67
+ /* ── Top bar ── */
68
+ '#topbar{display:flex;justify-content:space-between;align-items:center;gap:8px}' +
69
+ '#slide-name{font-size:13px;font-weight:600;color:#c8c8c8;letter-spacing:.04em;' +
70
+ 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}' +
71
+ '#timer{font-variant-numeric:tabular-nums;font-size:20px;font-weight:700;' +
72
+ 'color:#e0e0e0;letter-spacing:.1em;white-space:nowrap}' +
73
+ '#timer.warn{color:#f59e0b}#timer.over{color:#ef4444}' +
74
+
75
+ /* ── Previews row ── */
76
+ '#previews{display:grid;grid-template-columns:1fr 1fr;gap:12px}' +
77
+ '.pv-col{display:flex;flex-direction:column;gap:5px}' +
78
+ '.pv-label{font-size:10px;color:#555;letter-spacing:.08em;text-transform:uppercase}' +
79
+ '.pv-box{position:relative;width:100%;overflow:hidden;border-radius:5px;' +
80
+ 'background:#000;border:1px solid #1e1e1e}' +
81
+ '.pv-box iframe{position:absolute;top:0;left:0;width:1280px;height:720px;' +
82
+ 'border:none;pointer-events:none;transform-origin:top left}' +
83
+ '.pv-empty{display:grid;place-items:center;font-size:11px;' +
84
+ 'color:#444;font-style:italic}' +
85
+
86
+ /* ── Notes area ── */
87
+ '#notes-area{display:flex;flex-direction:column;gap:5px;min-height:0}' +
88
+ '#notes{background:#141414;border:1px solid #2a2a2a;border-radius:6px;' +
89
+ 'padding:12px 16px;font-size:14px;line-height:1.65;overflow-y:auto;' +
90
+ 'white-space:pre-wrap;color:#d0d0d0;flex:1;min-height:0}' +
91
+ '#notes.empty{color:#555;font-style:italic}' +
92
+
93
+ /* ── Bottom bar ── */
94
+ '#bottom{display:flex;align-items:center;gap:8px;flex-wrap:wrap;' +
95
+ 'border-top:1px solid #1e1e1e;padding-top:8px}' +
96
+ '#jump{display:flex;align-items:center;gap:5px;font-size:12px;color:#666}' +
97
+ '#jump-input{width:44px;background:#1e1e1e;border:1px solid #333;' +
98
+ 'border-radius:4px;color:#ccc;font:13px system-ui;padding:3px 6px;' +
99
+ 'text-align:center;-moz-appearance:textfield}' +
100
+ '#jump-input::-webkit-inner-spin-button{-webkit-appearance:none}' +
101
+ '#jump-total{color:#444;font-size:11px}' +
102
+ '#spacer{flex:1}' +
103
+ 'button{background:#1e1e1e;border:1px solid #333;border-radius:4px;' +
104
+ 'color:#aaa;font:12px system-ui;padding:4px 10px;cursor:pointer}' +
105
+ 'button:hover{background:#2a2a2a;color:#eee}' +
106
+ '</style></head><body>' +
107
+
108
+ '<div id="wrap">' +
109
+ ' <div id="topbar">' +
110
+ ' <span id="slide-name">\u2014</span><span id="timer">00:00</span>' +
111
+ ' </div>' +
112
+ ' <div id="previews">' +
113
+ ' <div class="pv-col">' +
114
+ ' <div class="pv-label">Now showing</div>' +
115
+ ' <div class="pv-box" id="curr-box"><div class="pv-empty" style="height:140px">\u2014</div></div>' +
116
+ ' </div>' +
117
+ ' <div class="pv-col">' +
118
+ ' <div class="pv-label">Up next</div>' +
119
+ ' <div class="pv-box" id="next-box"><div class="pv-empty" style="height:140px">(last slide)</div></div>' +
120
+ ' </div>' +
121
+ ' </div>' +
122
+ ' <div id="notes-area">' +
123
+ ' <div class="pv-label">Speaker notes</div>' +
124
+ ' <div id="notes" class="empty">(no speaker notes)</div>' +
125
+ ' </div>' +
126
+ ' <div id="bottom">' +
127
+ ' <div id="jump">Slide <input id="jump-input" type="number" min="1" value="1">' +
128
+ ' <span id="jump-total">/ \u2014</span></div>' +
129
+ ' <span id="spacer"></span>' +
130
+ ' <button id="btn-reset">Reset timer</button>' +
131
+ ' <button id="btn-close">Close</button>' +
132
+ ' </div>' +
133
+ '</div>' +
134
+
135
+ '<script>' +
136
+ // Scale preview iframes to fill their boxes (slides are 1280×720)
137
+ 'function updateScale(){' +
138
+ ' ["curr","next"].forEach(function(id){' +
139
+ ' var box=document.getElementById(id+"-box");' +
140
+ ' var f=box&&box.querySelector("iframe");' +
141
+ ' if(!box||!f)return;' +
142
+ ' var w=box.clientWidth;var scale=w/1280;' +
143
+ ' box.style.height=Math.round(720*scale)+"px";' +
144
+ ' f.style.transform="scale("+scale+")";' +
145
+ ' });' +
146
+ '}' +
147
+ 'window.addEventListener("resize",updateScale);' +
148
+
149
+ // Receive state updates from the deck window
150
+ 'window.addEventListener("message",function(e){' +
151
+ ' var d=e.data;if(!d||d.type!=="octocode-slides:presenter-update")return;' +
152
+
153
+ // Notes
154
+ ' var nEl=document.getElementById("notes");' +
155
+ ' if(d.notes){nEl.textContent=d.notes;nEl.className=""}' +
156
+ ' else{nEl.textContent="(no speaker notes)";nEl.className="empty"}' +
157
+
158
+ // Slide name
159
+ ' document.getElementById("slide-name").textContent=d.name||"\u2014";' +
160
+
161
+ // Jump control
162
+ ' if(typeof d.totalSlides==="number"){' +
163
+ ' document.getElementById("jump-total").textContent="/ "+d.totalSlides;' +
164
+ ' document.getElementById("jump-input").max=d.totalSlides;' +
165
+ ' document.getElementById("jump-input").value=(d.currentIndex||0)+1;' +
166
+ ' }' +
167
+
168
+ // Current slide preview
169
+ ' var cBox=document.getElementById("curr-box");' +
170
+ ' if(d.currUrl){' +
171
+ ' var cf=cBox.querySelector("iframe");' +
172
+ ' if(!cf||cf.src!==d.currUrl){' +
173
+ ' cBox.innerHTML=\'<iframe src="\'+d.currUrl+\'"></iframe>\';' +
174
+ ' updateScale();' +
175
+ ' }' +
176
+ ' }' +
177
+
178
+ // Next slide preview
179
+ ' var nBox=document.getElementById("next-box");' +
180
+ ' if(d.nextUrl){' +
181
+ ' var nf=nBox.querySelector("iframe");' +
182
+ ' if(!nf||nf.src!==d.nextUrl){' +
183
+ ' nBox.innerHTML=\'<iframe src="\'+d.nextUrl+\'"></iframe>\';' +
184
+ ' updateScale();' +
185
+ ' }' +
186
+ ' }else{' +
187
+ ' nBox.innerHTML=\'<div class="pv-empty" style="height:140px">(last slide)</div>\';' +
188
+ ' }' +
189
+ '});' +
190
+
191
+ // Jump-to-slide: send index to opener so deck navigates
192
+ 'document.getElementById("jump-input").addEventListener("change",function(){' +
193
+ ' var n=parseInt(this.value,10);if(!isFinite(n))return;' +
194
+ ' window.opener&&window.opener.postMessage(' +
195
+ ' {type:"octocode-slides:presenter-goto",index:n-1},"*");' +
196
+ '});' +
197
+
198
+ 'document.getElementById("btn-reset").onclick=function(){' +
199
+ ' window.opener&&window.opener.postMessage(' +
200
+ ' {type:"octocode-slides:presenter-reset"},"*")};' +
201
+ 'document.getElementById("btn-close").onclick=function(){window.close()};' +
202
+ '<\/script></body></html>';
203
+
204
+ // ── Timer ───────────────────────────────────────────────────────────────
205
+ function startTimer() {
206
+ if (!startTime) startTime = Date.now() - elapsed * 1000;
207
+ // Persist start time so a page refresh doesn't reset the clock
208
+ try { localStorage.setItem(LS_KEY, JSON.stringify({ startTime: startTime })); }
209
+ catch (_) {}
210
+
211
+ clearInterval(timerRef);
212
+ timerRef = setInterval(function () {
213
+ if (!win || win.closed) { clearInterval(timerRef); return; }
214
+ elapsed = Math.floor((Date.now() - startTime) / 1000);
215
+ var m = Math.floor(elapsed / 60);
216
+ var s = elapsed % 60;
217
+ var str = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
218
+ var timerEl = win.document.getElementById('timer');
219
+ if (!timerEl) return;
220
+ timerEl.textContent = str;
221
+ timerEl.className = elapsed > 3600 ? 'over' : elapsed > 2700 ? 'warn' : '';
222
+ }, 1000);
223
+ }
224
+
225
+ // ── Read speaker notes from active iframe ───────────────────────────────
226
+ function readNotes(frame) {
227
+ try {
228
+ var el = frame && frame.contentDocument &&
229
+ frame.contentDocument.querySelector('.speaker-notes');
230
+ if (!el) return '';
231
+ return (el.textContent || el.innerText || '').trim();
232
+ } catch (_) {
233
+ return '(speaker notes unavailable — serve from same origin: npx serve .)';
234
+ }
235
+ }
236
+
237
+ function frameLoaded(frame) {
238
+ try {
239
+ return !!(frame && frame.contentDocument &&
240
+ frame.contentDocument.readyState === 'complete');
241
+ } catch (_) {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ // ── Push update to popup ────────────────────────────────────────────────
247
+ // Sends: slide names, speaker notes, slide URLs (for live previews),
248
+ // total count and current index (for jump control).
249
+ function push() {
250
+ if (!win || win.closed) return;
251
+ var idx = getCurrentIndex();
252
+ var curr = playable[idx];
253
+ var next = playable[idx + 1];
254
+ var frame = getActiveFrame();
255
+ // Compute absolute base URL so iframe srcs work in the popup window
256
+ // (popup is opened with '', so relative URLs would not resolve).
257
+ var deckBase = location.href.replace(/[^\/]*$/, '');
258
+
259
+ function doSend() {
260
+ win.postMessage({
261
+ type: 'octocode-slides:presenter-update',
262
+ name: curr ? curr.name : '\u2014',
263
+ next: next ? next.name : null,
264
+ notes: readNotes(frame),
265
+ currUrl: curr ? deckBase + curr.path : null,
266
+ nextUrl: next ? deckBase + next.path : null,
267
+ totalSlides: playable.length,
268
+ currentIndex: idx
269
+ }, '*');
270
+ }
271
+
272
+ if (!frame || frameLoaded(frame)) {
273
+ doSend();
274
+ } else {
275
+ frame.addEventListener('load', doSend, { once: true });
276
+ }
277
+ }
278
+
279
+ // ── Open popup ──────────────────────────────────────────────────────────
280
+ function open() {
281
+ if (win && !win.closed) { win.focus(); return; }
282
+ win = window.open('', 'octocode-presenter',
283
+ 'width=720,height=520,resizable=yes,scrollbars=no,toolbar=no,menubar=no');
284
+ if (!win) { console.warn('[presenter] Popup blocked — allow popups for this origin.'); return; }
285
+ win.document.open();
286
+ win.document.write(POPUP_HTML);
287
+ win.document.close();
288
+ // startTimer picks up startTime from localStorage restore if already running
289
+ startTimer();
290
+ setTimeout(push, 200); // let popup render first
291
+ }
292
+
293
+ // ── Listen for messages from popup ──────────────────────────────────────
294
+ window.addEventListener('message', function (e) {
295
+ if (!e.data) return;
296
+ if (e.data.type === 'octocode-slides:presenter-reset') {
297
+ elapsed = 0;
298
+ startTime = Date.now();
299
+ // Clear persisted timer so a refresh after reset starts from 00:00
300
+ try { localStorage.removeItem(LS_KEY); } catch (_) {}
301
+ // Re-persist the fresh start time
302
+ try { localStorage.setItem(LS_KEY, JSON.stringify({ startTime: startTime })); }
303
+ catch (_) {}
304
+ }
305
+ });
306
+
307
+ // ── Slide change hook ───────────────────────────────────────────────────
308
+ // Call push() whenever the slide changes.
309
+ // index.html should call presenter.onSlideChange() after go().
310
+ return {
311
+ open: open,
312
+ push: push,
313
+ /** Call this after go() in index.html to update the popup. */
314
+ onSlideChange: push
315
+ };
316
+ }