Skip to content

UI only preview server (+ deployments)#2919

Merged
sawka merged 4 commits intomainfrom
sawka/preview-server
Feb 23, 2026
Merged

UI only preview server (+ deployments)#2919
sawka merged 4 commits intomainfrom
sawka/preview-server

Conversation

@sawka
Copy link
Member

@sawka sawka commented Feb 23, 2026

adds a new "preview server" for UI testing. hooking up to cloudflare pages deployments. for now, just a test "about modal" component. will add more later.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Walkthrough

This PR introduces a component preview server infrastructure within the frontend by adding a new /frontend/preview directory containing a Vite configuration, HTML entry point, and dynamic preview component discovery system. The codebase refactors two modal components: AboutModal is split into a wrapper and a public AboutModalV component with a props interface, and Tab is refactored to use an internal TabInner component wrapped by memo. The Taskfile gains new preview and build:preview tasks, the postinstall script is extracted to a dedicated file with conditional electron-builder execution, and Vite glob typing is added to support dynamic imports.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'UI only preview server (+ deployments)' directly and clearly describes the main changes: adding a preview server for UI components and related deployment configurations.
Description check ✅ Passed The description adequately explains the changeset: introducing a preview server for UI testing with Cloudflare Pages integration and an initial about modal component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/preview-server

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

process.exit(0);
}

import("child_process").then(({ execSync }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

WARNING: Missing error handling for dynamic import

The dynamic import() returns a Promise that could reject. If the import fails, the script will silently fail without any error message, making debugging difficult.

Consider adding error handling:

import("child_process").then(({ execSync }) => {
    execSync("electron-builder install-app-deps", { stdio: "inherit" });
}).catch((err) => {
    console.error("postinstall: failed to import child_process:", err);
    process.exit(1);
});

@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Feb 23, 2026

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
postinstall.cjs 9 Missing error handling for dynamic import - could fail silently
Files Reviewed (12 files)
  • Taskfile.yml - No issues
  • frontend/app/modals/about.tsx - No issues (good refactoring for testability)
  • frontend/app/tab/tab.tsx - No issues (good refactoring)
  • frontend/preview/index.html - No issues
  • frontend/preview/preview.css - No issues
  • frontend/preview/preview.tsx - No issues
  • frontend/preview/previews/.gitkeep - No issues
  • frontend/preview/previews/modal-about.preview.tsx - No issues
  • frontend/preview/vite.config.ts - No issues
  • frontend/types/media.d.ts - No issues
  • package.json - No issues
  • postinstall.cjs - 1 issue

Fix these issues in Kilo Cloud

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/app/tab/tab.tsx (1)

80-235: Avoid passing null to a mouse handler (type safety).

handleRenameTab is typed as a mouse handler but is invoked with null from the context menu. Consider a small refactor to keep the types honest and avoid null usage.

♻️ Suggested refactor
-const handleRenameTab: React.MouseEventHandler<HTMLDivElement> = (event) => {
+const startRename = (event?: React.MouseEvent<HTMLDivElement>) => {
     event?.stopPropagation();
     setIsEditable(true);
     editableTimeoutRef.current = setTimeout(() => {
         selectEditableText();
     }, 50);
 };
...
-    menu.push(
-        { label: "Rename Tab", click: () => handleRenameTab(null) },
+    menu.push(
+        { label: "Rename Tab", click: () => startRename() },
...
-    onDoubleClick={handleRenameTab}
+    onDoubleClick={startRename}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tab.tsx` around lines 80 - 235, The handler handleRenameTab
is typed as a mouse event handler but the context-menu code calls it with null;
fix by introducing a neutral starter function (e.g., startRenameTab) that
contains the rename logic (setIsEditable, editableTimeoutRef,
selectEditableText) and keeping handleRenameTab as a proper MouseEvent handler
that just calls startRenameTab(event); update the context menu entry to call
startRenameTab() instead of handleRenameTab(null) and leave
onDoubleClick/onClick usage to call handleRenameTab so types remain correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/modals/about.tsx`:
- Around line 74-76: Remove the unused AboutModalProps interface and the empty
destructured parameter in the AboutModal component signature: change the
component to a no-props declaration (e.g., use AboutModal() or const AboutModal
= () => ...) and delete the AboutModalProps interface and the "{}:
AboutModalProps" portion so lint errors for unused/empty props are resolved.

In `@frontend/preview/preview.tsx`:
- Around line 70-75: The index/back links currently use a hardcoded href="/"
which breaks when the app is served from a subpath; update the two anchor links
(the "← index" anchors at the shown locations) to preserve the current pathname
while clearing query params instead of pointing to "/", e.g. obtain the current
path from the router or window.location.pathname and navigate to that pathname
with an empty query (use Next.js useRouter().push/replace({ pathname:
router.pathname, query: {} }) or build href from window.location.pathname) so
the link works under subpaths and only removes the query string.
- Around line 30-34: The lazy loader currently falls back to
Object.values(mod)[0] which can pick the wrong export; update the loader inside
the Object.entries(previewModules).map callback (the lazy(() =>
loader().then(...)) block) to explicitly resolve the export: if mod.default
exists use it; otherwise examine the named exports (filtering out common module
metadata like __esModule), if there's exactly one remaining named export return
that as the component; otherwise throw a clear Error that includes the preview
path/key (use pathToKey(path)) and lists available export names so the failure
is explicit and actionable.

---

Nitpick comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 80-235: The handler handleRenameTab is typed as a mouse event
handler but the context-menu code calls it with null; fix by introducing a
neutral starter function (e.g., startRenameTab) that contains the rename logic
(setIsEditable, editableTimeoutRef, selectEditableText) and keeping
handleRenameTab as a proper MouseEvent handler that just calls
startRenameTab(event); update the context menu entry to call startRenameTab()
instead of handleRenameTab(null) and leave onDoubleClick/onClick usage to call
handleRenameTab so types remain correct.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 195277d and 63d2a2f.

📒 Files selected for processing (12)
  • Taskfile.yml
  • frontend/app/modals/about.tsx
  • frontend/app/tab/tab.tsx
  • frontend/preview/index.html
  • frontend/preview/preview.css
  • frontend/preview/preview.tsx
  • frontend/preview/previews/.gitkeep
  • frontend/preview/previews/modal-about.preview.tsx
  • frontend/preview/vite.config.ts
  • frontend/types/media.d.ts
  • package.json
  • postinstall.cjs

Comment on lines +74 to +76
interface AboutModalProps {}

const AboutModal = ({}: AboutModalProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove empty props interface and empty destructuring.

The empty interface and {} pattern trigger lint errors; a no-props component is clearer and lint-safe.

♻️ Suggested fix
-interface AboutModalProps {}
-
-const AboutModal = ({}: AboutModalProps) => {
+const AboutModal = () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface AboutModalProps {}
const AboutModal = ({}: AboutModalProps) => {
const AboutModal = () => {
🧰 Tools
🪛 Biome (2.4.4)

[error] 74-74: An empty interface is equivalent to {}.

(lint/suspicious/noEmptyInterface)


[error] 76-76: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/modals/about.tsx` around lines 74 - 76, Remove the unused
AboutModalProps interface and the empty destructured parameter in the AboutModal
component signature: change the component to a no-props declaration (e.g., use
AboutModal() or const AboutModal = () => ...) and delete the AboutModalProps
interface and the "{}: AboutModalProps" portion so lint errors for unused/empty
props are resolved.

Comment on lines +30 to +34
Object.entries(previewModules).map(([path, loader]) => [
pathToKey(path),
lazy(() =>
loader().then((mod) => ({ default: (mod.default ?? Object.values(mod)[0]) as React.ComponentType }))
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "preview.tsx" -type f

Repository: wavetermdev/waveterm

Length of output: 134


🌐 Web query:

React.lazy module shape requirements what must resolved module export default

💡 Result:

React.lazy requires that the function you pass returns a Promise that resolves to a “module object” with a default export—and that default must be a valid React component (function/class). In other words, it must resolve to:

{ default: SomeComponent }

React’s docs explicitly note that this pattern requires the lazily imported component to be exported as the default export. [1]

If your file only has a named export, adapt the import by mapping it into the required module shape:

const Foo = React.lazy(() =>
  import('./Foo').then((mod) => ({ default: mod.Foo }))
);

This works because you’re transforming the resolved value into the { default: ... } shape that React.lazy expects. [2]

Common failure mode: returning a module without default (or with default: undefined) triggers errors like “element type is invalid” / “promise resolves to undefined,” because React.lazy can’t find a component at module.default. [3]

Sources: [1] React docs on lazy and default exports, [2] example pattern for named exports, [3] discussion of missing default export causing React.lazy errors.


🏁 Script executed:

cat -n ./frontend/preview/preview.tsx | head -50

Repository: wavetermdev/waveterm

Length of output: 2697


🏁 Script executed:

cat -n ./frontend/preview/preview.tsx | sed -n '38,80p'

Repository: wavetermdev/waveterm

Length of output: 2013


🏁 Script executed:

# Check if any of the preview files exist and see how they're structured
find ./frontend/preview/previews -name "*.preview.tsx" -type f | head -3

Repository: wavetermdev/waveterm

Length of output: 115


🏁 Script executed:

cat -n ./frontend/preview/previews/modal-about.preview.tsx | head -30

Repository: wavetermdev/waveterm

Length of output: 514


🏁 Script executed:

# Check how many preview files exist and sample a few to see export patterns
find ./frontend/preview/previews -name "*.preview.tsx" -type f | xargs -I {} bash -c 'echo "=== {} ===" && head -20 {}'

Repository: wavetermdev/waveterm

Length of output: 476


Consider hardening preview module export resolution with explicit fallback.

The fallback to Object.values(mod)[0] is intentional (per the comment on line 16) to support both default and named exports, but is fragile: it's order-dependent and could select an unintended export if a file exports multiple values. Since preview files like modal-about.preview.tsx use named exports only, make the export selection explicit and fail fast with a clear error.

🔧 Suggested change
-        lazy(() =>
-            loader().then((mod) => ({ default: (mod.default ?? Object.values(mod)[0]) as React.ComponentType }))
-        ),
+        lazy(() =>
+            loader().then((mod) => {
+                const component =
+                    (mod.default as React.ComponentType | undefined) ??
+                    (mod.Preview as React.ComponentType | undefined);
+                if (!component) {
+                    throw new Error(`Preview module "${path}" must export default or Preview`);
+                }
+                return { default: component };
+            })
+        ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object.entries(previewModules).map(([path, loader]) => [
pathToKey(path),
lazy(() =>
loader().then((mod) => ({ default: (mod.default ?? Object.values(mod)[0]) as React.ComponentType }))
),
Object.entries(previewModules).map(([path, loader]) => [
pathToKey(path),
lazy(() =>
loader().then((mod) => {
const component =
(mod.default as React.ComponentType | undefined) ??
(mod.Preview as React.ComponentType | undefined);
if (!component) {
throw new Error(`Preview module "${path}" must export default or Preview`);
}
return { default: component };
})
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/preview/preview.tsx` around lines 30 - 34, The lazy loader currently
falls back to Object.values(mod)[0] which can pick the wrong export; update the
loader inside the Object.entries(previewModules).map callback (the lazy(() =>
loader().then(...)) block) to explicitly resolve the export: if mod.default
exists use it; otherwise examine the named exports (filtering out common module
metadata like __esModule), if there's exactly one remaining named export return
that as the component; otherwise throw a clear Error that includes the preview
path/key (use pathToKey(path)) and lists available export names so the failure
is explicit and actionable.

Comment on lines +70 to +75
<a
href="/"
className="flex items-center gap-1.5 text-accent text-sm hover:opacity-80 transition-opacity font-mono"
>
← index
</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid absolute “/” for index/back links.

Using href="/" assumes the preview app is served from the domain root. If this is deployed under a subpath (e.g., Pages preview), the links will break. Prefer clearing the query while keeping the current path.

🔧 Suggested change
-                href="/"
+                href="?"
-                    <a href="/" className="text-accent text-sm hover:opacity-80">
+                    <a href="?" className="text-accent text-sm hover:opacity-80">

Also applies to: 105-106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/preview/preview.tsx` around lines 70 - 75, The index/back links
currently use a hardcoded href="/" which breaks when the app is served from a
subpath; update the two anchor links (the "← index" anchors at the shown
locations) to preserve the current pathname while clearing query params instead
of pointing to "/", e.g. obtain the current path from the router or
window.location.pathname and navigate to that pathname with an empty query (use
Next.js useRouter().push/replace({ pathname: router.pathname, query: {} }) or
build href from window.location.pathname) so the link works under subpaths and
only removes the query string.

@sawka sawka merged commit 9abd590 into main Feb 23, 2026
10 checks passed
@sawka sawka deleted the sawka/preview-server branch February 23, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant