Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
66b8558
fix(agent): reconcile plan progress and refresh resumed session instr…
1052326311 Oct 2, 2026
f8cd7a4
feat(i18n): add persistent English and Simplified Chinese interface
1052326311 Oct 2, 2026
eca31c1
fix(chat): recover bounded provider timeouts and retain failure history
1052326311 Oct 2, 2026
7a4aae3
merge: retain timeout recovery and localized failure notices
1052326311 Oct 2, 2026
0e5a93c
fix(chat): retain active long-running streams across refreshes
1052326311 Oct 2, 2026
5b2ce5c
Merge branch 'fix/agent-state-and-session-recovery' into feat/interfa…
1052326311 Oct 2, 2026
26059a0
test(chat): type history and status response assertions
1052326311 Oct 2, 2026
3672c97
Merge branch 'fix/agent-state-and-session-recovery' into feat/interfa…
1052326311 Oct 2, 2026
b901c57
fix(agent): validate incomplete file writes before effects
1052326311 Oct 2, 2026
33495ed
Merge branch 'fix/agent-state-and-session-recovery' into feat/interfa…
1052326311 Oct 2, 2026
e68c709
fix(projects): serialize dependency installs and validate preview scr…
1052326311 Oct 2, 2026
3342679
test(preview): type captured script bodies
1052326311 Oct 2, 2026
feac79b
fix(agent): honor explicit container isolation for command execution
1052326311 Oct 2, 2026
ff9ddea
Merge branch 'fix/agent-state-and-session-recovery' into feat/interfa…
1052326311 Oct 2, 2026
abc9694
docs(agent): describe recovery and execution validation boundaries
1052326311 Oct 2, 2026
473a176
Merge branch 'fix/agent-state-and-session-recovery' into feat/interfa…
1052326311 Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3,905 changes: 3,905 additions & 0 deletions apps/web/messages/en.json

Large diffs are not rendered by default.

3,903 changes: 3,903 additions & 0 deletions apps/web/messages/index.json

Large diffs are not rendered by default.

39,435 changes: 39,435 additions & 0 deletions apps/web/messages/source.json

Large diffs are not rendered by default.

3,905 changes: 3,905 additions & 0 deletions apps/web/messages/zh-CN.json

Large diffs are not rendered by default.

44 changes: 32 additions & 12 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const nextConfig: NextConfig = {
output: "standalone",
Expand All @@ -17,7 +18,11 @@ const nextConfig: NextConfig = {
allowedDevOrigins: (() => {
const fromUrl = (u: string | undefined) => {
if (!u) return null;
try { return new URL(u).hostname; } catch { return null; }
try {
return new URL(u).hostname;
} catch {
return null;
}
};
const apex = (host: string) => {
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return host;
Expand All @@ -26,10 +31,16 @@ const nextConfig: NextConfig = {
};
// Dev-only cross-origin allowlist. Derive from the install env; never
// hardcode our own domain so self-hosters don't carry `*.doable.me`.
const host = fromUrl(process.env.NEXT_PUBLIC_APP_URL)
|| fromUrl(process.env.NEXT_PUBLIC_API_URL)
|| "";
return [host, host ? `*.${apex(host)}` : "", "localhost", "127.0.0.1"].filter(Boolean);
const host =
fromUrl(process.env.NEXT_PUBLIC_APP_URL) ||
fromUrl(process.env.NEXT_PUBLIC_API_URL) ||
"";
return [
host,
host ? `*.${apex(host)}` : "",
"localhost",
"127.0.0.1",
].filter(Boolean);
})(),
experimental: {
serverActions: {
Expand All @@ -54,17 +65,21 @@ const nextConfig: NextConfig = {
},
async redirects() {
return [
{ source: '/settings/ai', destination: '/ai-settings', permanent: false },
{ source: '/settings/usage', destination: '/usage', permanent: false },
{ source: '/settings/billing', destination: '/billing', permanent: false },
{ source: "/settings/ai", destination: "/ai-settings", permanent: false },
{ source: "/settings/usage", destination: "/usage", permanent: false },
{
source: "/settings/billing",
destination: "/billing",
permanent: false,
},
];
},
async rewrites() {
return [
// Bare /favicon.ico requests (crawlers, browsers without <link rel="icon">)
// are served by the dynamic icon.tsx route. Standalone build doesn't auto-alias
// icon.tsx to /favicon.ico, so we wire it explicitly.
{ source: '/favicon.ico', destination: '/icon' },
{ source: "/favicon.ico", destination: "/icon" },
];
},
async headers() {
Expand Down Expand Up @@ -111,7 +126,13 @@ const nextConfig: NextConfig = {
// allow="microphone; autoplay".
const apiUrlRaw = process.env.NEXT_PUBLIC_API_URL || "";
const previewOrigin = apiUrlRaw
? (() => { try { return new URL(apiUrlRaw).origin; } catch { return ""; } })()
? (() => {
try {
return new URL(apiUrlRaw).origin;
} catch {
return "";
}
})()
: "";
const micList = previewOrigin ? `(self "${previewOrigin}")` : "(self)";
// BUG-016: CSP was applying `unsafe-eval` + `unsafe-inline` to every
Expand Down Expand Up @@ -206,5 +227,4 @@ const nextConfig: NextConfig = {
},
};

export default nextConfig;

export default createNextIntlPlugin("./src/i18n/request.ts")(nextConfig);
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"lucide-react": "^0.468.0",
"monaco-editor": "^0.55.1",
"next": "^16.2.0",
"next-intl": "4.14.8",
"qrcode": "^1.5.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
Expand All @@ -48,8 +49,8 @@
"@opennextjs/cloudflare": "^1.17.1",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.10.5",
"@types/react": "^19.2.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
Expand Down
17 changes: 13 additions & 4 deletions apps/web/src/app/(auth)/error.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { useUiText } from "@/i18n/use-ui-text";

import { useEffect } from "react";
import Link from "next/link";
Expand All @@ -10,26 +11,34 @@ export default function AuthError({
error: Error & { digest?: string };
reset: () => void;
}) {
const ui = useUiText();

useEffect(() => {
console.error("Auth error:", error);
}, [error]);

return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#0a0a0a] px-4">
<h2 className="text-xl font-semibold text-white mb-2">Authentication Error</h2>
<p className="text-sm text-zinc-400 mb-6">{error.message || "Something went wrong."}</p>
<h2 className="text-xl font-semibold text-white mb-2">
{ui("Authentication Error")}
</h2>
<p className="text-sm text-zinc-400 mb-6">
{ui(error.message) || ui("Something went wrong.")}
</p>
<div className="flex gap-3">
<button
onClick={reset}
className="rounded-lg bg-zinc-800 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-700 transition-colors"
>
Try again
{" "}
{ui("Try again")}{" "}
</button>
<Link
href="/login"
className="rounded-lg bg-brand-600 px-4 py-2 text-sm text-white hover:bg-brand-500 transition-colors"
>
Back to Login
{" "}
{ui("Back to Login")}{" "}
</Link>
</div>
</div>
Expand Down
52 changes: 32 additions & 20 deletions apps/web/src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { useUiText } from "@/i18n/use-ui-text";

import { useState, type FormEvent } from "react";
import Link from "next/link";
Expand All @@ -10,6 +11,8 @@ import { ArrowLeft, Loader2, Mail } from "lucide-react";
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";

export default function ForgotPasswordPage() {
const ui = useUiText();

const [email, setEmail] = useState("");
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -28,16 +31,18 @@ export default function ForgotPasswordPage() {
});

if (!res.ok) {
const body = await res.json().catch(() => ({ error: "Request failed" }));
throw new Error(body.error ?? "Request failed");
const body = await res
.json()
.catch(() => ({ error: "Request failed" }));
throw new Error(body.error ?? ui("Request failed"));
}

setIsSubmitted(true);
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError("Something went wrong. Please try again.");
setError(ui("Something went wrong. Please try again."));
}
} finally {
setIsLoading(false);
Expand All @@ -51,14 +56,18 @@ export default function ForgotPasswordPage() {
<Mail className="h-6 w-6 text-green-600 dark:text-green-400" />
</div>
<h2 className="mb-2 text-xl font-semibold text-[hsl(var(--foreground))]">
Check your email
{" "}
{ui("Check your email")}{" "}
</h2>
<p className="mb-6 text-sm text-[hsl(var(--muted-foreground))]">
If an account exists for <strong className="text-[hsl(var(--foreground))]">{email}</strong>,
we&apos;ve sent password reset instructions to your inbox.
{" "}
{ui("If an account exists for")}{" "}
<strong className="text-[hsl(var(--foreground))]">{email}</strong>
{ui(", we've sent password reset instructions to your inbox.")}{" "}
</p>
<p className="mb-6 text-xs text-[hsl(var(--muted-foreground))]">
Didn&apos;t receive the email? Check your spam folder or{" "}
{" "}
{ui("Didn't receive the email? Check your spam folder or")}{" "}
<button
type="button"
onClick={() => {
Expand All @@ -67,15 +76,15 @@ export default function ForgotPasswordPage() {
}}
className="font-medium text-brand-700 hover:underline"
>
try again
{" "}
{ui("try again")}{" "}
</button>
</p>
<Link
href="/login"
className="inline-flex items-center gap-1.5 text-sm font-medium text-brand-700 hover:underline"
>
<ArrowLeft className="h-4 w-4" />
Back to sign in
<ArrowLeft className="h-4 w-4" /> {ui("Back to sign in")}{" "}
</Link>
</div>
);
Expand All @@ -88,10 +97,14 @@ export default function ForgotPasswordPage() {
<Mail className="h-6 w-6 text-brand-700" />
</div>
<h2 className="text-xl font-semibold text-[hsl(var(--foreground))]">
Forgot your password?
{" "}
{ui("Forgot your password?")}{" "}
</h2>
<p className="mt-2 text-sm text-[hsl(var(--muted-foreground))]">
No worries. Enter your email and we&apos;ll send you reset instructions.
{" "}
{ui(
"No worries. Enter your email and we'll send you reset instructions.",
)}{" "}
</p>
</div>

Expand All @@ -105,16 +118,16 @@ export default function ForgotPasswordPage() {
>
<path d="M8 1a7 7 0 100 14A7 7 0 008 1zm-.75 4.75a.75.75 0 011.5 0v3a.75.75 0 01-1.5 0v-3zM8 11a1 1 0 110 2 1 1 0 010-2z" />
</svg>
<span>{error}</span>
<span>{ui(error)}</span>
</div>
)}

<div className="space-y-2">
<Label htmlFor="email">Email address</Label>
<Label htmlFor="email">{ui("Email address")}</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
placeholder={ui("you@example.com")}
autoComplete="email"
required
disabled={isLoading}
Expand All @@ -131,11 +144,11 @@ export default function ForgotPasswordPage() {
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
<Loader2 className="mr-2 h-4 w-4 animate-spin" />{" "}
{ui("Sending...")}{" "}
</>
) : (
"Send reset instructions"
ui("Send reset instructions")
)}
</Button>
</form>
Expand All @@ -145,8 +158,7 @@ export default function ForgotPasswordPage() {
href="/login"
className="inline-flex items-center gap-1.5 text-sm font-medium text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Back to sign in
<ArrowLeft className="h-4 w-4" /> {ui("Back to sign in")}{" "}
</Link>
</p>
</>
Expand Down
Loading