Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
27e4201
local: deploy issuese
captjreacher Sep 28, 2025
c2470ba
Fix SSR render import for React DOM
captjreacher Sep 28, 2025
2a523ae
Merge pull request #1 from captjreacher/codex/fix-import-for-renderto…
captjreacher Sep 28, 2025
68ad41f
Refine SSR stream import usage
captjreacher Sep 28, 2025
2c45eb2
Merge branch 'main' into codex/fix-import-for-rendertoreadablestream
captjreacher Sep 28, 2025
0e830fc
Merge pull request #2 from captjreacher/codex/fix-import-for-renderto…
captjreacher Sep 28, 2025
ee09295
Fix SSR render stream import fallback
captjreacher Sep 28, 2025
1dc79a1
Merge branch 'main' into codex/fix-import-for-rendertoreadablestream
captjreacher Sep 28, 2025
c3bae0d
Merge pull request #3 from captjreacher/codex/fix-import-for-renderto…
captjreacher Sep 28, 2025
c9968a0
Fix server/client markup mismatch for hydration
captjreacher Sep 28, 2025
9632fe6
Merge pull request #4 from captjreacher/codex/troubleshoot-hydration-…
captjreacher Sep 28, 2025
7a03ec7
Align server shell with client markup and theme init
captjreacher Sep 28, 2025
6a8a9a4
Merge pull request #5 from captjreacher/codex/troubleshoot-hydration-…
captjreacher Sep 28, 2025
a1ae83e
Restore Remix document layout to fix hydration
captjreacher Sep 28, 2025
861f0c0
Merge branch 'main' into codex/troubleshoot-hydration-error
captjreacher Sep 28, 2025
45fe73b
Merge pull request #6 from captjreacher/codex/troubleshoot-hydration-…
captjreacher Sep 28, 2025
30c61f5
Restore root layout and error boundary
captjreacher Sep 28, 2025
1dcac12
Merge pull request #7 from captjreacher/codex/fix-unexpected-end-of-f…
captjreacher Sep 28, 2025
3e32883
Add API chat route
captjreacher Sep 29, 2025
0b805c8
Merge pull request #8 from captjreacher/codex/fix-routing-error-for-/…
captjreacher Sep 29, 2025
bfdd884
Handle missing Anthropic API key in chat action
captjreacher Sep 29, 2025
c4f759e
Merge branch 'main' into codex/fix-routing-error-for-/api/chat
captjreacher Sep 29, 2025
4cfc5e3
Merge pull request #9 from captjreacher/codex/fix-routing-error-for-/…
captjreacher Sep 29, 2025
fdb131b
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
06b8235
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
a60ba1b
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
a6b60e3
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
9cd96ff
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
f27518a
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
27188cb
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
9580968
fix(remix): resolve hydration error by wrapping app in layout
google-labs-jules[bot] Sep 29, 2025
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ dist-ssr
*.vars
.wrangler
_worker.bundle

.dev.vars
4 changes: 3 additions & 1 deletion app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { RemixBrowser } from '@remix-run/react';
import { startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';
import "@unocss/reset/tailwind.css";
import "virtual:uno.css";

startTransition(() => {
hydrateRoot(document.getElementById('root')!, <RemixBrowser />);
});
});
74 changes: 21 additions & 53 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,34 @@
import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare';
import { RemixServer } from '@remix-run/react';
import { isbot } from 'isbot';
import { renderToReadableStream } from 'react-dom/server';
import { renderHeadToString } from 'remix-island';
import { Head } from './root';
import { themeStore } from '~/lib/stores/theme';

import ReactDOMServer from 'react-dom/server';
import * as ReactDOMServerBrowser from 'react-dom/server.browser';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
_loadContext: AppLoadContext,
) {
const readable = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
});

const body = new ReadableStream({
start(controller) {
const head = renderHeadToString({ request, remixContext, Head });

controller.enqueue(
new Uint8Array(
new TextEncoder().encode(
`<!DOCTYPE html><html lang="en" data-theme="${themeStore.value}"><head>${head}</head><body><div id="root" class="w-full h-full">`,
),
),
);

const reader = readable.getReader();

function read() {
reader
.read()
.then(({ done, value }) => {
if (done) {
controller.enqueue(new Uint8Array(new TextEncoder().encode(`</div></body></html>`)));
controller.close();

return;
}

controller.enqueue(value);
read();
})
.catch((error) => {
controller.error(error);
readable.cancel();
});
}
read();
const serverExports = ReactDOMServer as typeof import('react-dom/server');
const browserServerExports =
ReactDOMServerBrowser as typeof import('react-dom/server.browser');

const renderToReadableStream =
typeof serverExports.renderToReadableStream === 'function'
? serverExports.renderToReadableStream
: browserServerExports.renderToReadableStream;

const readable = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
},

cancel() {
readable.cancel();
},
});
);

if (isbot(request.headers.get('user-agent') || '')) {
await readable.allReady;
Expand All @@ -71,7 +39,7 @@ export default async function handleRequest(
responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');

return new Response(body, {
return new Response(readable, {
headers: responseHeaders,
status: responseStatusCode,
});
Expand Down
2 changes: 1 addition & 1 deletion app/lib/.server/llm/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ToolResult<Name extends string, Args, Result> {
}

interface Message {
role: 'user' | 'assistant';
role: 'system' | 'user' | 'assistant';
content: string;
toolInvocations?: ToolResult<string, unknown, unknown>[];
}
Expand Down
3 changes: 2 additions & 1 deletion app/lib/runtime/action-runner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WebContainer } from '@webcontainer/api';
import { map, type MapStore } from 'nanostores';
import * as nodePath from 'node:path';
import * as nodePath from 'path';
import type { BoltAction } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
Expand Down Expand Up @@ -184,3 +184,4 @@ export class ActionRunner {
this.actions.setKey(id, { ...actions[id], ...newState });
}
}

5 changes: 3 additions & 2 deletions app/lib/stores/files.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
import { getEncoding } from 'istextorbinary';
import { map, type MapStore } from 'nanostores';
import { Buffer } from 'node:buffer';
import * as nodePath from 'node:path';
import { Buffer } from 'buffer';
import * as nodePath from 'path';
import { bufferWatchEvents } from '~/utils/buffer';
import { WORK_DIR } from '~/utils/constants';
import { computeFileModifications } from '~/utils/diff';
Expand Down Expand Up @@ -218,3 +218,4 @@ function convertToBuffer(view: Uint8Array): Buffer {

return buffer as Buffer;
}

6 changes: 6 additions & 0 deletions app/remix-server-build.d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module "../build/server/index.js" {
// Good enough for editor; Remix will provide the real thing at runtime
// You can refine this to ServerBuild if you want stricter types.
const build: any;
export = build;
}
134 changes: 65 additions & 69 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,79 @@
import { useStore } from '@nanostores/react';
import type { LinksFunction } from '@remix-run/cloudflare';
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
import tailwindReset from '@unocss/reset/tailwind-compat.css?url';
import { themeStore } from './lib/stores/theme';
import { stripIndents } from './utils/stripIndent';
import { createHead } from 'remix-island';
import { useEffect } from 'react';
// app/root.tsx
import type { LinksFunction } from "@remix-run/cloudflare";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
useRouteError,
} from "@remix-run/react";
import type { ReactNode } from "react";

import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
import globalStyles from './styles/index.scss?url';
import xtermStyles from '@xterm/xterm/css/xterm.css?url';
// Side-effect CSS imports (keep these; no ?url)
import "@unocss/reset/tailwind.css";
import "virtual:uno.css";

import 'virtual:uno.css';
import { DEFAULT_THEME, kTheme } from "~/lib/stores/theme";

const themeInitScript = `(() => {
try {
const storedTheme = localStorage.getItem(${JSON.stringify(kTheme)});
if (storedTheme) {
document.documentElement.setAttribute('data-theme', storedTheme);
}
} catch {}
})();`;

export const links: LinksFunction = () => [
{
rel: 'icon',
href: '/favicon.svg',
type: 'image/svg+xml',
},
{ rel: 'stylesheet', href: reactToastifyStyles },
{ rel: 'stylesheet', href: tailwindReset },
{ rel: 'stylesheet', href: globalStyles },
{ rel: 'stylesheet', href: xtermStyles },
{
rel: 'preconnect',
href: 'https://fonts.googleapis.com',
},
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
},
{ rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
];

const inlineThemeCode = stripIndents`
setTutorialKitTheme();
// 👇 This is what entry.server.tsx expects to exist
export default function App() {
return (
<Layout>
<Outlet />
</Layout>
);
}

function setTutorialKitTheme() {
let theme = localStorage.getItem('bolt_theme');
export function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" data-theme={DEFAULT_THEME}>
<head>
<Meta />
<Links />
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
</head>
<body className="min-h-dvh bg-neutral-950 text-white antialiased">
<div id="root" className="h-full w-full">
{children}
</div>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export function ErrorBoundary() {
const error = useRouteError();
let message = "Something went wrong";

document.querySelector('html')?.setAttribute('data-theme', theme);
if (isRouteErrorResponse(error)) {
message = `${error.status} ${error.statusText}`;
} else if (error instanceof Error) {
message = error.message;
}
`;

export const Head = createHead(() => (
<>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<script dangerouslySetInnerHTML={{ __html: inlineThemeCode }} />
</>
));

export function Layout({ children }: { children: React.ReactNode }) {
const theme = useStore(themeStore);

useEffect(() => {
document.querySelector('html')?.setAttribute('data-theme', theme);
}, [theme]);

return (
<>
{children}
<ScrollRestoration />
<Scripts />
</>
<Layout>
<div className="flex h-full flex-col items-center justify-center gap-2 p-6 text-center">
<h1 className="text-2xl font-semibold">Application error</h1>
<p className="max-w-lg text-balance text-neutral-300">{message}</p>
</div>
</Layout>
);
}

export default function App() {
return <Outlet />;
}
64 changes: 6 additions & 58 deletions app/routes/api.chat.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,7 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
export { action } from "./app.chat";

export async function action(args: ActionFunctionArgs) {
return chatAction(args);
}

async function chatAction({ context, request }: ActionFunctionArgs) {
const { messages } = await request.json<{ messages: Messages }>();

const stream = new SwitchableStream();

try {
const options: StreamingOptions = {
toolChoice: 'none',
onFinish: async ({ text: content, finishReason }) => {
if (finishReason !== 'length') {
return stream.close();
}

if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
throw Error('Cannot continue message: Maximum segments reached');
}

const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;

console.log(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);

messages.push({ role: 'assistant', content });
messages.push({ role: 'user', content: CONTINUE_PROMPT });

const result = await streamText(messages, context.cloudflare.env, options);

return stream.switchSource(result.toAIStream());
},
};

const result = await streamText(messages, context.cloudflare.env, options);

stream.switchSource(result.toAIStream());

return new Response(stream.readable, {
status: 200,
headers: {
contentType: 'text/plain; charset=utf-8',
},
});
} catch (error) {
console.log(error);

throw new Response(null, {
status: 500,
statusText: 'Internal Server Error',
});
}
}
export const loader = () =>
new Response("Method Not Allowed", {
status: 405,
headers: { Allow: "POST" },
});
Loading
Loading