Skip to content
Open
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
0f2b824
Copy review-editor and editor as new embeddable packages
backnotprop May 19, 2026
acfd540
Add useSessionFetch hook and SessionProvider
backnotprop May 19, 2026
53cdbc3
Migrate shared hooks to useSessionFetch
backnotprop May 19, 2026
d85d037
Migrate code review package to useSessionFetch
backnotprop May 19, 2026
0208efb
Export ReviewAppEmbedded without standalone providers
backnotprop May 19, 2026
4319c53
Mount code review surface in frontend session route
backnotprop May 19, 2026
b36d6cf
Fix session surface integration issues
backnotprop May 19, 2026
fe7af68
Resolve ~ to home directory in addProject
backnotprop May 19, 2026
dd82544
Fix duplicate Tailwind build causing style conflicts
backnotprop May 19, 2026
54f4513
Clean up CSS: remove duplications, fix keyframe collision
backnotprop May 19, 2026
60d3f06
Make sidebar logo link to homepage
backnotprop May 19, 2026
db7b053
Match sidebar trigger hover style to code review buttons
backnotprop May 19, 2026
bcd7fd6
Fix sidebar session labels and badge overlap
backnotprop May 19, 2026
c25d23a
Fix formatting
backnotprop May 19, 2026
3726bfc
Fix test suite: restore globalThis.fetch after useSessionFetch tests
backnotprop May 19, 2026
18a63e5
Add React Activity keep-alive for session surfaces
backnotprop May 19, 2026
b47cdaf
Fix: show error state when session load fails during active session
backnotprop May 19, 2026
4616847
Fix: deactivate session from Layout instead of inside hidden Activity
backnotprop May 19, 2026
2f8ff6f
Dispatch resize event when session becomes visible to fix Pierre diffs
backnotprop May 19, 2026
a6aa6ea
Replace Activity with visibility:hidden for session keep-alive
backnotprop May 19, 2026
b2cd5e8
Fix: derive landing page visibility from route match synchronously
backnotprop May 19, 2026
a18d819
Set router pendingMs to 0 to eliminate navigation delay
backnotprop May 19, 2026
17ef61d
Auto-restore code review drafts silently, remove restore dialog
backnotprop May 19, 2026
ca3f7f5
Fix flash of unstyled sidebar on page load
backnotprop May 19, 2026
89a1500
Style Toaster with theme tokens
backnotprop May 19, 2026
c5fcb79
Add rounded top-left corner to embedded code review
backnotprop May 19, 2026
f9a28f6
Move rounded corner to Layout session container and clip overflow
backnotprop May 19, 2026
21a156c
Fix curved border: move border from sidebar to session container
backnotprop May 19, 2026
22d0f87
Only show curved border when sidebar is open
backnotprop May 19, 2026
60eb1d2
Fix: use useSidebar hook for conditional curved border
backnotprop May 19, 2026
6eba8df
Fix project registry: key by cwd, defer registration until session su…
backnotprop May 19, 2026
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
1 change: 1 addition & 0 deletions apps/debug-frontend/src/daemon/events/hub-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export class DaemonHubClient {
this.socket = undefined;
this.daemonSubscribed = false;
socket?.close();
this.scheduleReconnect();
return;
}
if (
Expand Down
13 changes: 12 additions & 1 deletion apps/frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
<!doctype html>
<html lang="en">
<html lang="en" class="theme-neutral">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Plannotator</title>
<script>
// Apply saved theme before React mounts to prevent flash of unstyled content.
// ThemeProvider will take over once mounted.
try {
const ct = document.cookie.match(/(?:^|; )plannotator-color-theme=([^;]*)/);
const theme = ct ? decodeURIComponent(ct[1]) : 'neutral';
const mt = document.cookie.match(/(?:^|; )plannotator-theme=([^;]*)/);
const mode = mt ? decodeURIComponent(mt[1]) : 'dark';
document.documentElement.className = 'theme-' + theme + (mode === 'light' ? ' light' : '');
} catch(e) {}
</script>
</head>
<body>
<div id="root"></div>
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@fontsource-variable/geist-mono": "^5.2.7",
"@fontsource-variable/inter": "^5.2.8",
"@plannotator/code-review": "workspace:*",
"@plannotator/shared": "workspace:*",
"@plannotator/ui": "workspace:*",
"@radix-ui/react-collapsible": "^1.1.12",
Expand Down
71 changes: 60 additions & 11 deletions apps/frontend/src/app/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,85 @@
import { useCallback, useEffect } from "react";
import { Outlet } from "@tanstack/react-router";
import { Outlet, useMatchRoute } from "@tanstack/react-router";
import { Toaster } from "sonner";
import { SidebarProvider } from "@/components/ui/sidebar";
import { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { TooltipProvider } from "@/components/ui/tooltip";
import { AppSidebar } from "../components/sidebar/AppSidebar";
import { AddProjectDialog } from "../components/landing/AddProjectDialog";
import { SessionSurface } from "../components/sessions/SessionSurface";
import { useDaemonEvents } from "../daemon/events/use-daemon-events";
import { projectStore } from "../stores/project-store";
import { useAppStore } from "../stores/app-store";

export function Layout() {
function LayoutContent() {
const addProjectOpen = useAppStore((s) => s.addProjectOpen);
const setAddProjectOpen = useAppStore((s) => s.setAddProjectOpen);
const activeSessionId = useAppStore((s) => s.activeSessionId);
const visitedSessions = useAppStore((s) => s.visitedSessions);
const matchRoute = useMatchRoute();
const { open: sidebarOpen } = useSidebar();

useDaemonEvents();

useEffect(() => {
void projectStore.getState().fetchProjects();
}, []);

const isOnSession = !!matchRoute({ to: "/s/$sessionId", fuzzy: true });
const showLanding = !isOnSession;

const openAddProject = useCallback(() => setAddProjectOpen(true), [setAddProjectOpen]);

return (
<SidebarProvider
defaultOpen={false}
style={{ "--sidebar-width": "16rem" } as React.CSSProperties}
>
<>
<AppSidebar onAddProject={openAddProject} />
<main className="flex-1 overflow-hidden">
<Outlet />
<main className="relative flex-1 overflow-hidden">
<div
className="absolute inset-0"
style={{
visibility: showLanding ? "visible" : "hidden",
zIndex: showLanding ? 1 : 0,
}}
>
<Outlet />
</div>

{Object.values(visitedSessions).map(({ sessionId, bootstrap }) => (
<div
key={sessionId}
className={`absolute inset-0 overflow-hidden ${sidebarOpen ? "rounded-tl-xl border-l border-border/50" : ""}`}
style={{
visibility: sessionId === activeSessionId && isOnSession ? "visible" : "hidden",
zIndex: sessionId === activeSessionId && isOnSession ? 1 : 0,
}}
>
<SessionSurface bootstrap={bootstrap} />
</div>
))}
</main>
<AddProjectDialog open={addProjectOpen} onOpenChange={setAddProjectOpen} />
<Toaster position="bottom-right" />
</SidebarProvider>
<Toaster
position="bottom-right"
toastOptions={{
style: {
"--normal-bg": "var(--card)",
"--normal-border": "var(--border)",
"--normal-text": "var(--foreground)",
} as React.CSSProperties,
}}
/>
</>
);
}

export function Layout() {
return (
<TooltipProvider delayDuration={200} skipDelayDuration={100}>
<SidebarProvider
defaultOpen={false}
style={{ "--sidebar-width": "16rem" } as React.CSSProperties}
>
<LayoutContent />
</SidebarProvider>
</TooltipProvider>
);
}
2 changes: 2 additions & 0 deletions apps/frontend/src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export function createAppRouter(
routeTree,
context,
defaultPreload: "intent",
defaultPendingMs: 0,
defaultPendingMinMs: 0,
});
}

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function ProjectTable({
onSelect: (name: string) => void;
}) {
return (
<div className="overflow-hidden rounded-lg border border-border/60">
<div className="max-h-64 overflow-y-auto rounded-lg border border-border/60">
{projects.map((project, i) => (
<button
key={project.name}
Expand Down
54 changes: 54 additions & 0 deletions apps/frontend/src/components/sessions/SessionSurface.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { SidebarTrigger } from "@/components/ui/sidebar";
import { SessionProvider } from "@plannotator/ui/hooks/useSessionFetch";
import { ReviewAppEmbedded } from "@plannotator/code-review";
import "@plannotator/code-review/styles";
import type { SessionBootstrap } from "../../daemon/contracts";
import { getSessionModeMeta } from "../../shared/session-meta";

interface SessionSurfaceProps {
bootstrap: SessionBootstrap;
}

export function SessionSurface({ bootstrap }: SessionSurfaceProps) {
const { session } = bootstrap;

if (session.mode === "review") {
return (
<SessionProvider sessionId={session.id}>
<ReviewAppEmbedded
headerLeft={
<SidebarTrigger className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted" />
}
/>
</SessionProvider>
);
}

const meta = getSessionModeMeta(session.mode);
const Icon = meta.icon;

return (
<div className="isolate flex h-full flex-col bg-muted">
<nav className="flex h-10 shrink-0 items-center gap-2 px-3">
<SidebarTrigger className="-ml-1" />
<Icon className="size-4 text-muted-foreground" />
<span className="text-sm font-semibold">{session.label}</span>
<span className="rounded-full bg-surface-1 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{session.status}
</span>
</nav>

<div className="flex-1 overflow-hidden p-2 pt-0">
<div className="h-full overflow-hidden rounded-xl bg-card shadow-[var(--card-shadow)]">
<main className="h-full scroll-smooth overflow-auto">
<div className="mx-auto w-full max-w-3xl px-6 py-8 md:py-10">
<p className="text-sm text-muted-foreground">
{meta.label} surface · {session.project} · {session.id}
</p>
</div>
</main>
</div>
</div>
</div>
);
}
40 changes: 27 additions & 13 deletions apps/frontend/src/components/sidebar/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ import { getSessionModeMeta } from "../../shared/session-meta";

const MODE_ORDER = ["plan", "review", "annotate", "goal-setup", "archive"];

function formatSessionLabel(label: string): string {
return label
.replace(/^plugin-(plan|review|annotate|archive)-/, "")
.replace(/^(claude-code|opencode|pi|plannotator-frontend)-/, "")
.replace(/^goal-setup-(interview|facts)-/, "");
}

interface AppSidebarProps {
onAddProject: () => void;
}
Expand All @@ -46,20 +53,22 @@ export function AppSidebar({ onAddProject }: AppSidebarProps) {
}, [resolvedMode, setMode]);

return (
<Sidebar collapsible="offcanvas" className="border-r">
<Sidebar collapsible="offcanvas">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" className="pointer-events-none">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground text-xs font-bold">
P
</div>
<div className="grid flex-1 text-left leading-tight">
<span className="truncate text-sm font-semibold">Plannotator</span>
<span className="truncate text-[11px] text-muted-foreground">
{sessions.length} session{sessions.length !== 1 ? "s" : ""}
</span>
</div>
<SidebarMenuButton size="lg" asChild>
<Link to="/">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground text-xs font-bold">
P
</div>
<div className="grid flex-1 text-left leading-tight">
<span className="truncate text-sm font-semibold">Plannotator</span>
<span className="truncate text-[11px] text-muted-foreground">
{sessions.length} session{sessions.length !== 1 ? "s" : ""}
</span>
</div>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
Expand Down Expand Up @@ -87,7 +96,12 @@ export function AppSidebar({ onAddProject }: AppSidebarProps) {

return (
<SidebarMenuItem key={session.id}>
<SidebarMenuButton asChild isActive={isActive} tooltip={session.label}>
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={session.label}
className="pr-7"
>
<Link to="/s/$sessionId" params={{ sessionId: session.id }}>
<Icon
className={cn(
Expand All @@ -101,7 +115,7 @@ export function AppSidebar({ onAddProject }: AppSidebarProps) {
isTerminal && "text-muted-foreground/60 line-through",
)}
>
{session.label}
{formatSessionLabel(session.label)}
</span>
</Link>
</SidebarMenuButton>
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/src/daemon/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface DaemonApiClient {
cwd: string,
name?: string,
): Promise<DaemonApiResult<{ ok: true; project: ProjectEntry }>>;
removeProject(name: string): Promise<DaemonApiResult<{ ok: true }>>;
removeProject(cwd: string): Promise<DaemonApiResult<{ ok: true }>>;
createReviewSession(cwd: string): Promise<DaemonApiResult<SessionResponse>>;
createArchiveSession(cwd: string): Promise<DaemonApiResult<SessionResponse>>;
}
Expand Down Expand Up @@ -442,12 +442,12 @@ export function createDaemonApiClient(options: DaemonApiClientOptions = {}): Dae
);
},

removeProject(name) {
removeProject(cwd) {
return requestJson(
fetchImpl,
joinUrl(options.baseUrl, `/daemon/projects/${encodeURIComponent(name)}`),
joinUrl(options.baseUrl, "/daemon/projects"),
isDeleteSessionResponse,
{ method: "DELETE" },
{ method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cwd }) },
);
},

Expand Down
3 changes: 0 additions & 3 deletions apps/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { ThemeProvider } from "@plannotator/ui/components/ThemeProvider";
import { createAppRouter } from "./app/router";
import "./styles.css";

document.cookie = "plannotator-color-theme=neutral; path=/; max-age=31536000; SameSite=Lax";
document.cookie = "plannotator-theme=dark; path=/; max-age=31536000; SameSite=Lax";

const rootElement = document.getElementById("root");

if (!rootElement) {
Expand Down
42 changes: 13 additions & 29 deletions apps/frontend/src/routes/s.$sessionId.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { SidebarTrigger } from "@/components/ui/sidebar";
import type { SessionBootstrap } from "../daemon/contracts";
import type { DaemonApiResult } from "../daemon/api/errors";
import { getSessionModeMeta } from "../shared/session-meta";
import { appStore } from "../stores/app-store";

const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{2,127}$/;

Expand All @@ -20,6 +21,15 @@ export const Route = createFileRoute("/s/$sessionId")({

function SessionRoute() {
const result: DaemonApiResult<SessionBootstrap> = Route.useLoaderData();
const { sessionId } = Route.useParams();

useEffect(() => {
if (result.ok) {
appStore.getState().activateSession(sessionId, result.data);
} else {
appStore.getState().deactivateSession();
}
}, [sessionId, result]);

if (!result.ok) {
return (
Expand All @@ -36,32 +46,6 @@ function SessionRoute() {
);
}

const { session } = result.data;
const meta = getSessionModeMeta(session.mode);
const Icon = meta.icon;

return (
<div className="isolate flex h-full flex-col bg-muted">
<nav className="flex h-10 shrink-0 items-center gap-2 px-3">
<SidebarTrigger className="-ml-1" />
<Icon className="size-4 text-muted-foreground" />
<span className="text-sm font-semibold">{session.label}</span>
<span className="rounded-full bg-surface-1 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{session.status}
</span>
</nav>

<div className="flex-1 overflow-hidden p-2 pt-0">
<div className="h-full overflow-hidden rounded-xl bg-card shadow-[var(--card-shadow)]">
<main className="h-full scroll-smooth overflow-auto">
<div className="mx-auto w-full max-w-3xl px-6 py-8 md:py-10">
<p className="text-sm text-muted-foreground">
{meta.label} surface · {session.project} · {session.id}
</p>
</div>
</main>
</div>
</div>
</div>
);
// The actual surface is rendered by Layout via Activity — this route just registers the session
return null;
}
Loading