Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions next/src/lib/agents/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export const AGENTS: AgentDef[] = [
{ id: "claude-opus-4-7", label: "claude-opus-4-7" },
{ id: "claude-sonnet-4-6", label: "claude-sonnet-4-6" },
{ id: "claude-haiku-4-5", label: "claude-haiku-4-5" },
// DeepSeek models (via `cc switch` or direct config) — kept in the
// Claude Code picker so users who route Claude through a custom model
// endpoint can switch models without leaving html-anything's UI.
{ id: "deepseek-v4-pro", label: "deepseek-v4-pro" },
{ id: "deepseek-v4-flash", label: "deepseek-v4-flash" },
Comment on lines +66 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These two DeepSeek entries are added to the Claude Code adapter's fallbackModels, but unlike the Claude/alias models above them they only resolve when the user has routed Claude Code through a custom model endpoint (e.g. via cc switch). For a user who has not done that, picking deepseek-v4-pro here makes invokeAgent run claude --model deepseek-v4-pro, which the stock Claude CLI/API rejects with an opaque "unknown model" failure surfaced as a failed run.

Why it matters: the new code comment documents this precondition for developers, but the user-facing label ("deepseek-v4-pro") gives the end user no hint that these require a routed setup. The picker therefore advertises two models that fail by default, which works against the "Curated, evidence-based model list" intent documented on AgentDef.fallbackModels (lines 37-40) and on DEFAULT_MODEL.

Suggested change: surface the precondition in the user-visible label, e.g. { id: "deepseek-v4-pro", label: "deepseek-v4-pro (routed endpoint)" } and likewise for deepseek-v4-flash; or gate these two entries behind a detected routed config so they only appear when usable. This is a nit, not merge-blocking.

🔁 Powered by Looper · runner=reviewer · agent=claude-code · An autonomous AI dev team for your GitHub repos.

],
},
{
Expand Down
26 changes: 24 additions & 2 deletions next/src/lib/export/deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,31 @@ async function renderSlideToBlob(slide: DeckSlide, scale = 2): Promise<Blob> {
const done = () => res();
if (iframe.contentDocument?.readyState === "complete") return done();
iframe.addEventListener("load", done, { once: true });
setTimeout(done, 4000);
setTimeout(done, 8000);
});
return await iframeToBlob(iframe, { scale });

// Give the browser an extra frame to finish layout — without this,
// Tailwind CDN / fonts / images injected via srcdoc can report a 0px
// scrollHeight on the first paint, causing iframeToBlob to throw
// "preview has no content yet" even though the live preview is fine.
await new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));

// Retry once if the first capture returns an empty document.
const maxAttempts = 2;
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await iframeToBlob(iframe, { scale });
} catch (err) {
lastError = err;
if (attempt < maxAttempts) {
// Wait longer before retry — fonts / Tailwind CDN may still be
// inflight even after the load event fired.
await new Promise<void>((r) => setTimeout(r, 1200));
}
}
}
throw lastError;
} finally {
wrap.remove();
}
Expand Down
13 changes: 11 additions & 2 deletions next/src/lib/export/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export async function nodeToBlob(node: HTMLElement, opts: ImageOpts = {}): Promi
* the exact viewport the browser used when measuring text. Using
* `scrollWidth` here causes a 1–2px drift that wraps Chinese titles to
* a new line and shoves them under the body text.
* To keep clientWidth reliable across platforms, overflow is set to
* "hidden" during capture — without this, Windows' always-visible
* scrollbar adds ~15px of chrome that narrows clientWidth and crops
* the left edge of the exported image.
* 4. Pass explicit width/height to modern-screenshot so the foreignObject
* SVG matches the laid-out size 1:1.
*/
Expand All @@ -172,8 +176,13 @@ export async function iframeToBlob(
const fullHeight = fullScrollHeight(doc);
if (!fullHeight) throw new Error("preview has no content yet");
iframe.style.height = `${fullHeight}px`;
doc.documentElement.style.overflow = "visible";
doc.body.style.overflow = "visible";
// Use overflow:hidden — not visible — to suppress platform scrollbars.
// On Windows where scrollbars are always visible, "visible" adds a ~15px
// vertical scrollbar that reduces clientWidth, causing the exported image
// to be cropped on the left side (the screenshot is taken at the narrowed
// viewport width and the rightmost pixels are lost).
doc.documentElement.style.overflow = "hidden";
doc.body.style.overflow = "hidden";

// Wait a couple of frames for the browser to re-flow at the new size.
await NEXT_FRAME();
Expand Down