octocode-cli 1.3.0 → 1.5.0
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.
- package/README.md +129 -28
- package/out/chunks/chunk-7476PETK.js +309 -0
- package/out/chunks/chunk-CVNNNSMQ.js +26 -0
- package/out/chunks/chunk-OQBJTZWK.js +60 -0
- package/out/chunks/chunk-UCZCF3BQ.js +9 -0
- package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
- package/out/chunks/commands-XBFPLHSQ.js +8 -0
- package/out/chunks/help-P7TCOYAJ.js +10 -0
- package/out/chunks/main-help-ULF5PAQY.js +10 -0
- package/out/chunks/prompts-5E6VKRX5.js +8 -0
- package/out/chunks/spinner-URV2OX6O.js +8 -0
- package/out/chunks/tool-command-M6VA7P2F.js +8 -0
- package/out/octocode-cli.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +60 -58
- package/skills/agentic-flow-best-practices/SKILL.md +280 -0
- package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
- package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
- package/skills/agentic-flow-best-practices/references/resources.md +112 -0
- package/skills/octocode-brainstorming/.env.example +11 -0
- package/skills/octocode-brainstorming/SKILL.md +262 -0
- package/skills/octocode-brainstorming/scripts/tavily-search.mjs +138 -0
- package/skills/octocode-chrome-devtools/README.md +541 -0
- package/skills/octocode-chrome-devtools/SKILL.md +197 -0
- package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
- package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
- package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
- package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
- package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
- package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
- package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
- package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
- package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
- package/skills/octocode-design/README.md +2 -2
- package/skills/octocode-documentation-writer/README.md +1 -1
- package/skills/octocode-engineer/README.md +1 -1
- package/skills/octocode-engineer/SKILL.md +137 -306
- package/skills/octocode-engineer/references/cli-reference.md +13 -0
- package/skills/octocode-engineer/references/output-files.md +3 -3
- package/skills/octocode-engineer/scripts/run.js +146 -146
- package/skills/octocode-engineer/src/pipeline/main.ts +1 -17
- package/skills/octocode-engineer/src/pipeline/progress.ts +4 -0
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
- package/skills/octocode-install/SKILL.md +1 -1
- package/skills/octocode-pull-request-reviewer/README.md +5 -5
- package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
- package/skills/octocode-research/AGENTS.md +1 -1
- package/skills/octocode-research/README.md +2 -2
- package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
- package/skills/octocode-research/scripts/server.js +184 -239
- package/skills/octocode-research/src/routes/github.ts +4 -21
- package/skills/octocode-research/src/routes/local.ts +4 -21
- package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
- package/skills/octocode-search-skill/SKILL.md +337 -0
- package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
- package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
- package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
- package/skills/octocode-search-skill/references/install-reference.md +130 -0
- package/skills/octocode-search-skill/references/references-template.md +27 -0
- package/skills/octocode-search-skill/references/references.md +62 -0
- package/skills/octocode-slides/README.md +307 -0
- package/skills/octocode-slides/SKILL.md +410 -0
- package/skills/octocode-slides/references/01-brief.md +156 -0
- package/skills/octocode-slides/references/02-research.md +149 -0
- package/skills/octocode-slides/references/03-outline.md +172 -0
- package/skills/octocode-slides/references/04-design.md +301 -0
- package/skills/octocode-slides/references/05-implementation.md +213 -0
- package/skills/octocode-slides/references/06-review.md +258 -0
- package/skills/octocode-slides/references/animation.md +281 -0
- package/skills/octocode-slides/references/design-system.md +316 -0
- package/skills/octocode-slides/references/html-templates.md +673 -0
- package/skills/octocode-slides/references/image-generation.md +448 -0
- package/skills/octocode-slides/references/resources.md +840 -0
- package/skills/octocode-slides/references/slide-rules.md +541 -0
- package/skills/octocode-slides/references/wireframes.md +727 -0
- package/skills/octocode-slides/scripts/animation.js +182 -0
- package/skills/octocode-slides/scripts/base.css +353 -0
- package/skills/octocode-slides/scripts/base.html +655 -0
- package/skills/octocode-slides/scripts/generate_image.py +221 -0
- package/skills/octocode-slides/scripts/navbridge.js +79 -0
- package/skills/octocode-slides/scripts/presenter.js +316 -0
- package/skills/octocode-slides/scripts/slide.html +248 -0
- package/skills/octocode-stats/SKILL.md +73 -0
- package/skills/octocode-stats/assets/template.html +1332 -0
- package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
- package/out/chunks/chunk-LH4AZJPA.js +0 -389
- package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
- package/out/chunks/commands-M3QTWKWE.js +0 -51
- package/out/chunks/help-XPXP46ZT.js +0 -10
- package/out/chunks/main-help-HXFAFHPG.js +0 -10
- package/out/chunks/tool-command-VHFLPIHY.js +0 -8
|
@@ -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
|
+
}
|