Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
95ae4ab
Embed plan review surface and fix cross-surface issues (#758)
backnotprop May 27, 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
31 changes: 28 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ claude --plugin-dir ./apps/hook
**Config-only settings (`~/.plannotator/config.json`)**: Some settings have no env-var equivalent and are toggled by editing the config file directly:

- `pfmReminder` (`true` / `false`, default `false`) — when enabled, a Plannotator Flavored Markdown reminder is injected at plan-time describing the renderer's extensions (code-file links, callouts, tables, diagrams, task lists, hex swatches, wiki-links). Lets the planning agent enrich plans with PFM features without having to discover them. Composes cleanly with the compound-skill improvement hook. Supported across all three runtimes: Claude Code (`improve-context` PreToolUse hook in `apps/hook/server/index.ts`), OpenCode (`experimental.chat.system.transform` in `apps/opencode-plugin/index.ts`), and Pi (`before_agent_start` in `apps/pi-extension/index.ts`).
- `legacyTabMode` (`true` / `false`, default `false`) — when enabled, the daemon opens a new browser tab for every session regardless of whether a frontend is already connected. Sessions use the full-screen `CompletionOverlay` with auto-close instead of the inline `CompletionBanner`. Preserves the pre-frontend tab-per-session behavior for users who prefer it.

**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected when `PLANNOTATOR_REMOTE` is unset. Set `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `0` / `false` to force local mode.

Expand Down Expand Up @@ -234,11 +235,33 @@ The daemon is the single long-running Bun server used by normal plan/review/anno
| `/daemon/sessions/:id/cancel` | POST | Cancel a session and dispose its resources |
| `/daemon/sessions/:id` | DELETE | Delete a session record |
| `/daemon/shutdown` | POST | Ask the daemon to stop |
| `/daemon/ws` | WebSocket | Multiplex daemon lifecycle events, session-scoped external annotation events, agent job events, and correlated session actions |
| `/daemon/config` | GET | Read global config (`~/.plannotator/config.json`) |
| `/daemon/config` | POST | Write global config keys (allowlisted: `displayName`, `pfmReminder`, `legacyTabMode`, `diffOptions`, `conventionalComments`, `conventionalLabels`) |
| `/daemon/git/user` | GET | Return git user name from `git config user.name` |
| `/daemon/vaults` | GET | Detect available Obsidian vaults |
| `/daemon/obsidian/vaults` | GET | Alias for `/daemon/vaults` |
| `/daemon/hooks/status` | GET | Return PFM reminder and improvement hook status |
| `/daemon/projects` | DELETE | Remove a project by `?cwd=` (optional `?clean=1` to cancel active sessions) |
| `/daemon/projects/prs` | GET | List open PRs for a project (`?cwd=`) |
| `/daemon/projects/prs/detailed` | GET | List PRs with review metadata for dashboard (`?cwd=`) |
| `/daemon/fs/list` | GET | List directory contents (`?path=`) |
| `/daemon/ws` | WebSocket | Multiplex daemon lifecycle events, session-scoped external annotation events, agent job events, session revision events, and correlated session actions |
| `/s/:id` | GET | Serve the browser HTML for a session |
| `/s/:id/api/...` | Any | Route browser API requests to that session's plan/review/annotate handler |

Runtime live updates for daemon lifecycle events, external annotations, and agent jobs are delivered through `/daemon/ws`. Session-scoped updates subscribe by `{ family, sessionId }`. HTTP endpoints below remain for snapshots, mutations, uploads, and large payloads. AI query token streaming remains on `/api/ai/query`.
Runtime live updates for daemon lifecycle events, external annotations, agent jobs, and session revisions are delivered through `/daemon/ws`. Session-scoped updates subscribe by `{ family, sessionId }`. HTTP endpoints below remain for snapshots, mutations, uploads, and large payloads. AI query token streaming remains on `/api/ai/query`.

### Session Persistence and Resubmission

When a user denies a plan (or sends feedback on a review/annotation), the session enters `awaiting-resubmission` status instead of completing. The session's HTTP handler stays alive. When the agent replans and submits again via `POST /daemon/sessions`, the daemon matches the new submission to the existing session by a match key (`plan:project:slug` for plans, `review:project:branch` for reviews, `annotate:project:filePath` for single-file annotations). The session reactivates in place — the frontend receives a `session-revision` event via WebSocket with the updated content.

**Sessions never die.** No session type calls `store.complete()` from its decision handler. All sessions survive feedback, approve, and exit — the HTTP handler stays alive and the tab keeps working. `registerPersistentDecision` always calls `store.suspend()`. `registerReviewDecision` always calls `store.idle()`. Non-terminal sessions have no expiry timer.

**Session statuses (plan/annotate):** `active` → `awaiting-resubmission` (on any decision) → `active` (on resubmit) → `awaiting-resubmission` ... repeating.

**Session statuses (code review):** `active` → `idle` (on any decision) → `active` (on agent resubmit) → `idle` ... repeating.

**Event families:** `daemon`, `external-annotations`, `agent-jobs`, `session-revision`.

### Plan Server (`packages/server/index.ts`)

Expand Down Expand Up @@ -309,7 +332,9 @@ Runtime live updates for daemon lifecycle events, external annotations, and agen

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath, sourceInfo?, gate, renderAs?, rawHtml? }` |
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath, sourceInfo?, gate, renderAs?, rawHtml?, previousPlan, versionInfo }` |
| `/api/plan/version` | GET | Fetch specific version (`?v=N`) — single-file annotate only |
| `/api/plan/versions` | GET | List all versions — single-file annotate only |
| `/api/feedback` | POST | Submit annotations (body: feedback, annotations) |
| `/api/approve` | POST | Approve without feedback (review-gate UX, `--gate`) |
| `/api/exit` | POST | Close session without feedback |
Expand Down
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
5 changes: 5 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@
},
"dependencies": {
"@fontsource-variable/geist-mono": "^5.2.7",
"@fontsource-variable/instrument-sans": "^5.2.8",
"@fontsource-variable/inter": "^5.2.8",
"@plannotator/code-review": "workspace:*",
"@plannotator/plan-review": "workspace:*",
"@plannotator/shared": "workspace:*",
"@plannotator/ui": "workspace:*",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-router": "^1.141.0",
"class-variance-authority": "^0.7.1",
Expand Down
109 changes: 95 additions & 14 deletions apps/frontend/src/app/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,117 @@
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 { SidebarPeek } from "../components/sidebar/SidebarPeek";
import { AddProjectDialog } from "../components/landing/AddProjectDialog";
import { AppSettingsDialog } from "../components/settings/AppSettingsDialog";
import { SessionSurface } from "../components/sessions/SessionSurface";
import { appStore } from "../stores/app-store";
import { setGlobalFetchBase } from "@plannotator/ui/utils/api";
import { useDaemonEvents } from "../daemon/events/use-daemon-events";

setGlobalFetchBase("/daemon");
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();
const { reportActiveSession } = useDaemonEvents();

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

const openAddProject = useCallback(() => setAddProjectOpen(true), [setAddProjectOpen]);
const isOnSession = !!matchRoute({ to: "/s/$sessionId", fuzzy: true });

useEffect(() => {
reportActiveSession(isOnSession ? activeSessionId : null);
}, [reportActiveSession, isOnSession, activeSessionId]);
const showLanding = !isOnSession;

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === ",") {
e.preventDefault();
const current = appStore.getState().settingsOpen;
appStore.getState().setSettingsOpen(!current);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);

return (
<SidebarProvider
defaultOpen={false}
style={{ "--sidebar-width": "16rem" } as React.CSSProperties}
>
<AppSidebar onAddProject={openAddProject} />
<main className="flex-1 overflow-hidden">
<Outlet />
<>
<AppSidebar />
<SidebarPeek />
<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 }) => {
const isActive = sessionId === activeSessionId && isOnSession;
return (
<div
key={sessionId}
className={`absolute inset-0 overflow-hidden ${sidebarOpen ? "rounded-tl-xl border-l border-border/50" : ""}`}
style={{
visibility: isActive ? "visible" : "hidden",
contentVisibility: isActive ? "visible" : "hidden",
containIntrinsicSize: isActive ? undefined : "auto 100vh",
zIndex: isActive ? 1 : 0,
}}
>
<SessionSurface bootstrap={bootstrap} />
</div>
);
})}
</main>
<AddProjectDialog open={addProjectOpen} onOpenChange={setAddProjectOpen} />
<Toaster position="bottom-right" />
</SidebarProvider>
<AppSettingsDialog />
<Toaster
position="bottom-right"
toastOptions={{
style: {
"--normal-bg": "var(--card)",
"--normal-border": "var(--border)",
"--normal-text": "var(--foreground)",
"--normal-action-bg": "var(--primary)",
"--normal-action-text": "var(--primary-foreground)",
} as React.CSSProperties,
}}
/>
</>
);
}

export function Layout() {
const matchRoute = useMatchRoute();
const initiallyOnSession = !!matchRoute({ to: "/s/$sessionId", fuzzy: true });

return (
<TooltipProvider delayDuration={200} skipDelayDuration={100}>
<SidebarProvider
defaultOpen={!initiallyOnSession}
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading