From 0b7b6321060b6a5de09c9768dd36046260c4aa0b Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 18 May 2026 10:31:52 -0700 Subject: [PATCH 1/2] Add long-running Plannotator daemon runtime Single daemon process per machine manages session lifecycle, serves browser UIs at /s/, and exposes control APIs. CLI commands auto-start the daemon and create sessions via HTTP. Includes session store, auth tokens, lock files, event broadcasting, and goal-setup daemon integration. --- apps/hook/dev-mock-api.ts | 159 ------------------ apps/marketing/src/lib/shortcutReference.ts | 4 +- apps/skills/plannotator-setup-goal/SKILL.md | 109 ++----------- packages/editor/App.tsx | 172 +++++--------------- packages/editor/components/AppHeader.tsx | 69 ++------ packages/editor/shortcuts.ts | 27 --- packages/server/package.json | 11 +- packages/server/sessions.ts | 2 +- packages/ui/components/CommentPopover.tsx | 63 ++----- packages/ui/components/ConfirmDialog.tsx | 32 +--- packages/ui/components/core/button.tsx | 44 ----- packages/ui/components/core/textarea.tsx | 26 --- packages/ui/package.json | 6 +- packages/ui/shortcuts/index.ts | 1 - packages/ui/theme.css | 104 ------------ 15 files changed, 109 insertions(+), 720 deletions(-) delete mode 100644 packages/ui/components/core/button.tsx delete mode 100644 packages/ui/components/core/textarea.tsx diff --git a/apps/hook/dev-mock-api.ts b/apps/hook/dev-mock-api.ts index 7f2bcf595..65e837cd1 100644 --- a/apps/hook/dev-mock-api.ts +++ b/apps/hook/dev-mock-api.ts @@ -552,9 +552,6 @@ This change lands in section 3 of the contributor guide alongside the updated re const USE_DIFF_DEMO = process.env.VITE_DIFF_DEMO === "1" || process.env.VITE_DIFF_DEMO === "true"; -const GOAL_SETUP_DEMO = process.env.VITE_GOAL_SETUP_DEMO; -const USE_GOAL_SETUP_DEMO = - GOAL_SETUP_DEMO === "interview" || GOAL_SETUP_DEMO === "facts"; const PLAN_V1 = USE_DIFF_DEMO ? PLAN_V1_DIFF_TEST : PLAN_V1_DEFAULT; const PLAN_V2 = USE_DIFF_DEMO ? PLAN_V2_DIFF_TEST : PLAN_V2_DEFAULT; @@ -629,153 +626,6 @@ export function devMockApi(): Plugin { if (req.url === '/api/plan') { res.setHeader('Content-Type', 'application/json'); - if (USE_GOAL_SETUP_DEMO) { - res.end(JSON.stringify({ - plan: '', - origin: 'claude-code', - mode: 'goal-setup', - sharingEnabled: false, - goalSetup: GOAL_SETUP_DEMO === "facts" ? { - stage: "facts", - title: "Interactive goal setup facts", - goalSlug: "interactive-goal-setup-ui", - facts: [ - { - id: "skill-batch", - text: "The setup-goal skill should package all interview questions into one Plannotator UI session.", - accepted: false, - removed: false, - recommendedAutomatedVerification: true, - automatedVerification: true, - }, - { - id: "facts-verify", - text: "Each fact can be accepted, edited, removed, commented on, and marked for automated verification.", - accepted: false, - removed: false, - recommendedAutomatedVerification: true, - automatedVerification: true, - }, - { - id: "header-submit", - text: "Goal setup submission should use the Plannotator app header action area instead of local form buttons.", - accepted: false, - removed: false, - recommendedAutomatedVerification: false, - automatedVerification: false, - }, - { - id: "question-modes", - text: "The interview UI should cover text answers, single-select choices, multi-select choices, and custom option entry.", - accepted: false, - removed: false, - recommendedAutomatedVerification: true, - automatedVerification: true, - }, - { - id: "previous", - text: "Previously accepted facts remain visible in the facts review with their accepted state preserved.", - accepted: true, - removed: false, - recommendedAutomatedVerification: false, - automatedVerification: false, - }, - { - id: "bulk-accept", - text: "The facts UI provides a single action to accept every visible fact while keeping the review open for final edits.", - accepted: false, - removed: false, - recommendedAutomatedVerification: true, - automatedVerification: true, - }, - { - id: "copy-export", - text: "The interview and facts UIs can copy the current state as raw JSON or markdown for provenance and debugging.", - accepted: false, - removed: false, - recommendedAutomatedVerification: false, - automatedVerification: false, - }, - ], - } : { - stage: "interview", - title: "Interactive goal setup interview", - goalSlug: "interactive-goal-setup-ui", - questions: [ - { - id: "objective", - prompt: "What is the primary outcome of this goal?", - description: "One sentence that captures what 'done' looks like.", - answerMode: "text", - recommendedAnswer: "A bundled goal setup UI where agents launch one browser session for interview Q&A and a second for facts acceptance, replacing multi-turn chat prompting.", - }, - { - id: "audience", - prompt: "Which inferred audience assumption should change?", - description: "The agent should not need basic confirmation here; only change this if the default is wrong.", - answerMode: "single", - recommendedAnswer: "Developers using Claude Code with Plannotator installed.", - recommendedOptionIds: ["devs-cc"], - options: [ - { id: "devs-cc", label: "Developers on Claude Code" }, - { id: "devs-oc", label: "Developers on OpenCode" }, - { id: "devs-all", label: "All Plannotator users" }, - ], - }, - { - id: "scope", - prompt: "Which inferred scope items should stay or be added?", - description: "Recommended items are based on the code paths the agent can infer. Add only missing nuance.", - answerMode: "multi-custom", - recommendedAnswer: "Skill text, interactive UI, server endpoints, and tests.", - recommendedOptionIds: ["skill", "ui", "server", "tests"], - options: [ - { id: "skill", label: "Skill text" }, - { id: "ui", label: "Interactive UI" }, - { id: "server", label: "Server endpoints" }, - { id: "tests", label: "Tests and fixtures" }, - ], - }, - { - id: "launch", - prompt: "What rollout constraint should override the default?", - description: "Default is the smallest useful launch; choose a broader option only if runtime parity matters immediately.", - answerMode: "single", - recommendedOptionIds: ["claude-only"], - options: [ - { id: "claude-only", label: "Claude Code only" }, - { id: "all-runtimes", label: "All runtimes (Claude Code, OpenCode, Pi)" }, - { id: "prototype", label: "Prototype behind a dev flag" }, - ], - }, - { - id: "risk", - prompt: "Which risks should the plan explicitly address?", - answerMode: "multi", - recommendedOptionIds: ["runtime-parity", "data-loss"], - options: [ - { id: "runtime-parity", label: "Runtime parity", description: "Bun and Pi server endpoints stay mirrored." }, - { id: "data-loss", label: "Answer data loss", description: "Edited answers survive until submission." }, - { id: "header-actions", label: "Header action placement", description: "Submit/close matches existing patterns." }, - ], - }, - { - id: "facts-ux", - prompt: "How should fact review work?", - answerMode: "text", - recommendedAnswer: "Vertical list with per-fact accept, edit, remove, comment, and automated-verification toggle. Accepted facts hidden by default on re-review.", - }, - { - id: "out-of-scope", - prompt: "Anything explicitly out of scope?", - answerMode: "custom", - required: false, - }, - ], - }, - })); - return; - } res.end(JSON.stringify({ plan: undefined, // Editor uses its own DIFF_DEMO_PLAN_CONTENT origin: 'claude-code', @@ -786,15 +636,6 @@ export function devMockApi(): Plugin { return; } - if (req.url === '/api/goal-setup/submit' && req.method === 'POST') { - req.on('data', () => {}); - req.on('end', () => { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ ok: true })); - }); - return; - } - if (req.url === '/api/plan/versions') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ diff --git a/apps/marketing/src/lib/shortcutReference.ts b/apps/marketing/src/lib/shortcutReference.ts index 03f989dff..80663451c 100644 --- a/apps/marketing/src/lib/shortcutReference.ts +++ b/apps/marketing/src/lib/shortcutReference.ts @@ -1,11 +1,11 @@ -import { planReviewSurface, annotateSurface, goalSetupSurface } from '../../../../packages/editor/shortcuts'; +import { planReviewSurface, annotateSurface } from '../../../../packages/editor/shortcuts'; import { codeReviewSurface } from '../../../../packages/review-editor/shortcuts'; import { listRegistryShortcutSections } from '../../../../packages/ui/shortcuts'; import type { ShortcutSurface } from '../../../../packages/ui/shortcuts'; const slugify = (value: string) => value.toLowerCase().replace(/\s+/g, '-'); -const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, goalSetupSurface, codeReviewSurface]; +const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, codeReviewSurface]; export const shortcutReferenceSurfaces = allSurfaces.map((surface) => ({ ...surface, diff --git a/apps/skills/plannotator-setup-goal/SKILL.md b/apps/skills/plannotator-setup-goal/SKILL.md index 303c3b775..9f4f7ac3e 100644 --- a/apps/skills/plannotator-setup-goal/SKILL.md +++ b/apps/skills/plannotator-setup-goal/SKILL.md @@ -13,131 +13,46 @@ Turn an idea into a goal package at `goals//` through structured discovery State back what the user wants in your own words. If the conversation already has rich context, summarize it. If the goal is bare or vague, do minimal shallow exploration of the codebase to ground your understanding. Keep it to 2-3 sentences. Wait for the user to confirm or correct before continuing. -Create the goal directory once the slug is clear: +### 2. Interview (grill me) -```bash -mkdir -p goals/ -``` - -Use `goals//` for both working JSON files and final docs. The JSON files are provenance and iteration state; the markdown files are the human-readable authoritative goal package. - -**Browser session patience rule:** Plannotator goal setup is a user-driven browser session. After launching an interview or facts command, be absolutely patient and keep waiting on the user until they submit, dismiss, or explicitly ask you to stop. Do not close, kill, restart, refresh, or open a second copy just because the UI is idle or the user is taking time. Never close and reopen the session as a way to update state; if a rerun is needed after the prior session ends, update the working JSON file and launch a new command from that file. - -### 2. Interview Bundle - -Build a compact bundle of questions that can derive every "fact" this goal should produce. Package the questions together so the user can answer them quickly in the Plannotator goal setup UI. For each question, include your recommended answer and use options when they make answering faster. - -Do not ask obvious confirmation questions. If the answer can be inferred from the user's request, from the conversation, or from shallow codebase exploration, infer it and move on. If an obvious area has meaningful nuance, present the inferred answer as a recommendation with options or a custom "add/correct this" path rather than asking the user to restate the obvious. - -Question areas that usually matter: +Interview the user such that you can derive every "fact" this goal should produce, & until you reach a complete shared understanding of the desired outcomes. The following questions areas should help you determine facts about the outcome. - What the feature/change is - Who it's for - What problem it solves - What behavior changes - What success looks like -- What's in and out of scope (the most important area to determine facts) +- What's in and out of scope (The most important area to determine facts) - What edge cases to consider - What constraints or precedent apply -**If a question can be answered by exploring the codebase, explore the codebase instead of asking.** Only include questions where the user's judgment is actually needed. Prefer fewer, higher-leverage questions over exhaustive obvious ones. - -Write the interview bundle before showing it to the user: - -`goals//interview.json` - -```json -{ - "stage": "interview", - "title": "Short human-readable title", - "goalSlug": "", - "questions": [ - { - "id": "scope", - "prompt": "What should be in scope?", - "description": "Optional clarification.", - "answerMode": "multi-custom", - "recommendedAnswer": "Your recommended answer.", - "recommendedOptionIds": ["ui", "server"], - "options": [ - { "id": "ui", "label": "UI" }, - { "id": "server", "label": "Server" } - ], - "required": true - } - ] -} -``` - -Supported `answerMode` values: `text`, `single`, `multi`, `custom`, `single-custom`, `multi-custom`. +Ask questions **one at a time**, waiting for feedback before continuing. For each question, provide your recommended answer. Use the question/answer tool if available. -Run this as a monitored foreground process and wait patiently for the browser session to finish. The command may appear idle while the user is reading, editing, or asking questions; leave it running: +**If a question can be answered by exploring the codebase, explore the codebase instead of asking.** -```bash -plannotator setup-goal interview goals//interview.json --json -``` - -The command returns JSON on stdout with the submitted answers. Write that exact result to `goals//interview-result.json` before continuing. A convenient pattern is: - -```bash -plannotator setup-goal interview goals//interview.json --json | tee goals//interview-result.json -``` - -If the user revises after the session finishes, update `interview.json` and rerun the command instead of reconstructing the whole bundle from memory. If the session is dismissed, stop and tell the user the goal setup was closed. - -Before moving to facts, read every answer and note carefully: - -- If the user wrote questions, uncertainty, "not sure", "needs context", or similar concerns in an answer or note, stop and address those questions in chat. Do not proceed to facts until the user has enough context or you have rerun a revised interview bundle. -- If the user skipped a question with a note, treat the note as intentional feedback, not as an empty answer. Answer the note, refine the question, or make a documented assumption before proceeding. -- If the user skipped a question without a note, proceed only if the missing answer is non-blocking; otherwise ask the smallest possible follow-up in chat. +Stop when you feel confident in being able to describe the facts of the goal outcome. Don't pad. ### 3. Fact Sheet A fact is a simple description of each outcome of a goal. It should be easily testable and verifiable. A fact may describe the function of a specific feature or aspect of a system. A fact may determine specific UI and UX. Again, a fact is literally anything that can be tested and verified in automated or manual testing. Keep fact language simple. In a way, a fact sheet is a design spec, but less verbose & using language the human user can easily visualize & rationalize. -Prepare a facts review bundle from `goals//interview-result.json`. Each fact should include whether automated verification is recommended and preselected. - -Write the facts review bundle before showing it to the user. If revising after a prior facts pass, start from `facts-review.json` and `facts-result.json`, include previously accepted facts with `"accepted": true`, and preserve their state. - -`goals//facts-review.json` - -```json -{ - "stage": "facts", - "title": "Short human-readable title", - "goalSlug": "", - "facts": [ - { - "id": "fact-1", - "text": "The accepted fact text.", - "accepted": false, - "removed": false, - "recommendedAutomatedVerification": true, - "automatedVerification": true - } - ] -} -``` - -Run this as a monitored foreground process and wait patiently for the browser session to finish. The command may appear idle while the user is reviewing, editing, or asking questions; leave it running: +Create the goal directory and write `goals//facts.md` — a flat list of bulleted facts. Each fact is one line. Add a minimal note only when the fact can't be stated clearly on its own. ```bash -plannotator setup-goal facts goals//facts-review.json --json +mkdir -p goals/ ``` -The command returns JSON on stdout with accepted/edited/removed facts plus automated verification selections. Write that exact result to `goals//facts-result.json`. A convenient pattern is: +Gate the fact sheet with Plannotator: ```bash -plannotator setup-goal facts goals//facts-review.json --json | tee goals//facts-result.json +plannotator annotate goals//facts.md --gate ``` -Write `goals//facts.md` as a flat readable list of accepted facts. Each fact is one line; add a minimal note only when the fact cannot be stated clearly on its own. Also write `goals//facts.meta.json` preserving each accepted fact's `id`, final `text`, `comment`, `recommendedAutomatedVerification`, and `automatedVerification` value. - -If the user edits or removes facts in the UI, apply that result directly. If the session is dismissed, stop and tell the user the facts review was closed. +If denied, revise from feedback and re-gate until approved. ### 4. Plan -Explore the codebase. Discover and validate implementation paths toward each accepted fact. Treat facts with `automatedVerification: true` as requiring concrete automated checks unless you document a blocker. Trace through code, identify files and systems involved, surface risks and unknowns. Refine until you have a confident order of operations. +Explore the codebase. Discover and validate implementation paths toward each fact. Trace through code, identify files and systems involved, surface risks and unknowns. Refine until you have a confident order of operations. Write `goals//plan.md`: diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index c8e12d2e1..c351adc82 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -57,6 +57,7 @@ import { useArchive } from '@plannotator/ui/hooks/useArchive'; import { useEditorAnnotations } from '@plannotator/ui/hooks/useEditorAnnotations'; import { useExternalAnnotations } from '@plannotator/ui/hooks/useExternalAnnotations'; import { useExternalAnnotationHighlights } from '@plannotator/ui/hooks/useExternalAnnotationHighlights'; +import { getApiOriginAndBase } from '@plannotator/ui/utils/api'; import { buildPlanAgentInstructions } from '@plannotator/ui/utils/planAgentInstructions'; import { useFileBrowser } from '@plannotator/ui/hooks/useFileBrowser'; import { isVaultBrowserEnabled } from '@plannotator/ui/utils/obsidian'; @@ -68,12 +69,6 @@ import type { ArchivedPlan } from '@plannotator/ui/components/sidebar/ArchiveBro import { PlanDiffViewer } from '@plannotator/ui/components/plan-diff/PlanDiffViewer'; import { CodeFilePopout, type CodeFileAnnotationInput } from '@plannotator/ui/components/CodeFilePopout'; import type { PlanDiffMode } from '@plannotator/ui/components/plan-diff/PlanDiffModeSwitcher'; -import { - GoalSetupSurface, - type GoalSetupActionState, - type GoalSetupSurfaceHandle, -} from '@plannotator/ui/components/goal-setup/GoalSetupSurface'; -import type { GoalSetupBundle } from '@plannotator/shared/goal-setup'; // Demo content toggle. Default: the original Real-time Collaboration plan. // Opt-in diff-engine stress test: `VITE_DIFF_DEMO=1 bun run dev:hook` swaps // in the 20-case Auth Service Refactor test plan. dev-mock-api.ts reads the @@ -135,6 +130,23 @@ const App: React.FC = () => { // icon → labels hidden — fallback below that const planAreaRef = useRef(null); const [actionsLabelMode, setActionsLabelMode] = useState('full'); + // useLayoutEffect + synchronous getBoundingClientRect so the initial + // bucket is set before the browser paints. Otherwise narrow viewports + // get a one-frame flash of "Global comment"/"Copy plan" labels before + // the ResizeObserver callback collapses them. + useLayoutEffect(() => { + const el = planAreaRef.current; + if (!el) return; + const bucket = (w: number): ActionsLabelMode => + w >= 800 ? 'full' : w >= 680 ? 'short' : 'icon'; + setActionsLabelMode(bucket(el.getBoundingClientRect().width)); + const ro = new ResizeObserver(([entry]) => { + const next = bucket(entry.contentRect.width); + setActionsLabelMode((prev) => (prev === next ? prev : next)); + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); const [isApiMode, setIsApiMode] = useState(false); const [origin, setOrigin] = useState(null); const [gitUser, setGitUser] = useState(); @@ -143,14 +155,6 @@ const App: React.FC = () => { const [annotateMode, setAnnotateMode] = useState(false); const [gate, setGate] = useState(false); const [annotateSource, setAnnotateSource] = useState<'file' | 'message' | 'folder' | null>(null); - const [goalSetupBundle, setGoalSetupBundle] = useState(null); - const goalSetupSurfaceRef = useRef(null); - const [goalSetupAction, setGoalSetupAction] = useState({ - canSubmit: false, - isSubmitting: false, - submitted: false, - submitLabel: 'Submit', - }); const [sourceInfo, setSourceInfo] = useState(); const [sourceConverted, setSourceConverted] = useState(false); const [renderAs, setRenderAs] = useState<'markdown' | 'html'>('markdown'); @@ -172,7 +176,6 @@ const App: React.FC = () => { const [wideModeType, setWideModeType] = useState(null); const wideModeSnapshotRef = useRef(null); const lastAppliedTocEnabledRef = useRef(uiPrefs.tocEnabled); - const goalSetupMode = goalSetupBundle !== null; useEffect(() => { document.title = repoInfo ? `${repoInfo.display} · Plannotator` : "Plannotator"; @@ -552,7 +555,7 @@ const App: React.FC = () => { const activeSection = useActiveSection(containerRef, headingCount, scrollViewport); const { editorAnnotations, deleteEditorAnnotation } = useEditorAnnotations(); - const { externalAnnotations, updateExternalAnnotation, deleteExternalAnnotation } = useExternalAnnotations({ enabled: isApiMode && !goalSetupMode }); + const { externalAnnotations, updateExternalAnnotation, deleteExternalAnnotation } = useExternalAnnotations({ enabled: isApiMode }); // Drive DOM highlights for SSE-delivered external annotations. Disabled // while a linked doc overlay is open (Viewer DOM is hidden) and while the @@ -560,7 +563,7 @@ const App: React.FC = () => { const { reset: resetExternalHighlights } = useExternalAnnotationHighlights({ viewerRef, externalAnnotations, - enabled: isApiMode && !goalSetupMode && !linkedDocHook.isActive && !isPlanDiffActive, + enabled: isApiMode && !linkedDocHook.isActive && !isPlanDiffActive, planKey: markdown, }); @@ -640,32 +643,12 @@ const App: React.FC = () => { setRenderAs, ); - // useLayoutEffect + synchronous getBoundingClientRect so the initial - // bucket is set before the browser paints. Otherwise narrow viewports - // get a one-frame flash of "Global comment"/"Copy plan" labels before - // the ResizeObserver callback collapses them. - useLayoutEffect(() => { - if (isLoading && !isSharedSession) return; - - const el = planAreaRef.current; - if (!el) return; - const bucket = (w: number): ActionsLabelMode => - w >= 800 ? 'full' : w >= 680 ? 'short' : 'icon'; - setActionsLabelMode(bucket(el.getBoundingClientRect().width)); - const ro = new ResizeObserver(([entry]) => { - const next = bucket(entry.contentRect.width); - setActionsLabelMode((prev) => (prev === next ? prev : next)); - }); - ro.observe(el); - return () => ro.disconnect(); - }, [isLoading, isSharedSession]); - // Auto-save annotation drafts const { draftBanner, restoreDraft, dismissDraft } = useAnnotationDraft({ annotations: allAnnotations, codeAnnotations, globalAttachments, - isApiMode: isApiMode && !goalSetupMode, + isApiMode, isSharedSession, submitted: !!submitted, }); @@ -732,16 +715,12 @@ const App: React.FC = () => { if (!res.ok) throw new Error('Not in API mode'); return res.json(); }) - .then((data: { plan: string; origin?: Origin; mode?: 'annotate' | 'annotate-last' | 'annotate-folder' | 'archive' | 'goal-setup'; goalSetup?: GoalSetupBundle; filePath?: string; sourceInfo?: string; sourceConverted?: boolean; gate?: boolean; renderAs?: 'html' | 'markdown'; rawHtml?: string; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string; host?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string }; archivePlans?: ArchivedPlan[]; projectRoot?: string; isWSL?: boolean; serverConfig?: { displayName?: string; gitUser?: string } }) => { + .then((data: { plan: string; origin?: Origin; mode?: 'annotate' | 'annotate-last' | 'annotate-folder' | 'archive'; filePath?: string; sourceInfo?: string; sourceConverted?: boolean; gate?: boolean; renderAs?: 'html' | 'markdown'; rawHtml?: string; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string; host?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string }; archivePlans?: ArchivedPlan[]; projectRoot?: string; isWSL?: boolean; serverConfig?: { displayName?: string; gitUser?: string } }) => { // Initialize config store with server-provided values (config file > cookie > default) configStore.init(data.serverConfig); // gitUser drives the "Use git name" button in Settings; stays undefined (button hidden) when unavailable setGitUser(data.serverConfig?.gitUser); - if (data.mode === 'goal-setup' && data.goalSetup) { - setGoalSetupBundle(data.goalSetup); - setMarkdown(''); - setSharingEnabled(false); - } else if (data.mode === 'archive') { + if (data.mode === 'archive') { // Archive mode: show first archived plan or clear demo content setMarkdown(data.plan || ''); if (data.archivePlans) archive.init(data.archivePlans); @@ -766,7 +745,7 @@ const App: React.FC = () => { if (data.mode === 'annotate-folder') { sidebar.open('files'); } - if (data.mode === 'annotate' || data.mode === 'annotate-last' || data.mode === 'annotate-folder') { + if (data.mode && data.mode !== 'archive') { setAnnotateSource(data.mode === 'annotate-last' ? 'message' : data.mode === 'annotate-folder' ? 'folder' : 'file'); } setSourceInfo(data.sourceInfo ?? undefined); @@ -802,7 +781,7 @@ const App: React.FC = () => { if (data.origin) { setOrigin(data.origin); // For Claude Code, check if user needs to configure permission mode - if (data.origin === 'claude-code' && data.mode !== 'goal-setup' && needsPermissionModeSetup()) { + if (data.origin === 'claude-code' && needsPermissionModeSetup()) { setShowPermissionModeSetup(true); } // Load saved permission mode preference @@ -1107,43 +1086,22 @@ const App: React.FC = () => { } }, []); - const handleGoalSetupSubmit = useCallback(() => { - goalSetupSurfaceRef.current?.submit(); - }, []); - - const handleGoalSetupExit = useCallback(async () => { - setIsExiting(true); - try { - const res = await fetch('/api/exit', { method: 'POST' }); - if (res.ok) { - setSubmitted('exited'); - } else { - throw new Error('Failed to exit'); - } - } catch { - setIsExiting(false); - } - }, []); - // Global keyboard shortcuts (Cmd/Ctrl+Enter to submit) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Only handle Cmd/Ctrl+Enter if (e.key !== 'Enter' || !(e.metaKey || e.ctrlKey)) return; - const target = e.target as HTMLElement | null; - const tag = target?.tagName; - const isTextField = tag === 'INPUT' || tag === 'TEXTAREA' || Boolean(target?.isContentEditable); - - // Let active confirmation dialogs own Cmd/Ctrl+Enter and Escape. - if (document.querySelector('[data-plannotator-confirm-dialog="true"]')) return; + // Don't intercept if typing in an input/textarea + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA') return; // Don't intercept if any modal is open if (showExport || showImport || showFeedbackPrompt || showClaudeCodeWarning || showExitWarning || showAgentWarning || showPermissionModeSetup || pendingPasteImage) return; // Don't intercept if already submitted, submitting, or exiting - if (submitted || isSubmitting || isExiting || goalSetupAction.isSubmitting) return; + if (submitted || isSubmitting || isExiting) return; // Don't intercept in demo/share mode (no API) if (!isApiMode) return; @@ -1151,17 +1109,6 @@ const App: React.FC = () => { // Don't submit while viewing a linked doc if (linkedDocHook.isActive) return; - if (goalSetupMode) { - if (document.querySelector('[data-comment-popover="true"]')) return; - if (isTextField && !target?.closest('.goal-shell')) return; - e.preventDefault(); - if (goalSetupAction.canSubmit) goalSetupSurfaceRef.current?.submit(); - return; - } - - // Don't intercept if typing in an input/textarea outside goal setup. - if (isTextField) return; - e.preventDefault(); // Annotate mode: gate-enabled + no annotations → approve (empty stdout). @@ -1201,8 +1148,8 @@ const App: React.FC = () => { }, [ showExport, showImport, showFeedbackPrompt, showClaudeCodeWarning, showExitWarning, showAgentWarning, showPermissionModeSetup, pendingPasteImage, - submitted, isSubmitting, isExiting, goalSetupAction.isSubmitting, isApiMode, linkedDocHook.isActive, annotations.length, codeAnnotations.length, externalAnnotations.length, annotateMode, - gate, hasAnyAnnotations, goalSetupMode, goalSetupAction.canSubmit, + submitted, isSubmitting, isExiting, isApiMode, linkedDocHook.isActive, annotations.length, codeAnnotations.length, externalAnnotations.length, annotateMode, + gate, hasAnyAnnotations, origin, getAgentWarning, ]); @@ -1480,10 +1427,10 @@ const App: React.FC = () => { // Agent Instructions — copy a clipboard payload teaching external agents // (Claude Code, Codex, etc.) how to POST annotations into this session via - // /api/external-annotations. The instruction body lives in a separate module + // the session API base. The instruction body lives in a separate module // (utils/agentInstructions.ts) so it's easy to edit independently of UI code. const handleCopyAgentInstructions = async () => { - const payload = buildPlanAgentInstructions(window.location.origin); + const payload = buildPlanAgentInstructions(getApiOriginAndBase()); try { await navigator.clipboard.writeText(payload); toast.success('Agent instructions copied'); @@ -1670,14 +1617,6 @@ const App: React.FC = () => { const annotateReaderMaxWidth = canUseWideMode && wideModeType === 'wide' ? null : planMaxWidth; - if (isLoading && !isSharedSession) { - return ( - -
- - ); - } - return ( @@ -1686,10 +1625,6 @@ const App: React.FC = () => { isApiMode={isApiMode} annotateMode={annotateMode} archiveMode={archive.archiveMode} - goalSetupMode={goalSetupMode} - goalSetupCanSubmit={goalSetupAction.canSubmit} - goalSetupIsSubmitting={goalSetupAction.isSubmitting} - goalSetupSubmitLabel={goalSetupAction.submitLabel} gate={gate} isSharedSession={isSharedSession} origin={origin} @@ -1710,8 +1645,6 @@ const App: React.FC = () => { onCallbackFeedback={handleCallbackFeedback} onCallbackApprove={handleCallbackApprove} onAnnotateExit={handleHeaderAnnotateExit} - onGoalSetupExit={handleGoalSetupExit} - onGoalSetupSubmit={handleGoalSetupSubmit} onAnnotateFeedback={handleHeaderAnnotateFeedback} onAnnotateApprove={handleHeaderAnnotateApprove} onFeedback={handleHeaderFeedback} @@ -1734,7 +1667,7 @@ const App: React.FC = () => { onSaveToBear={handleSaveToBear} onSaveToOctarine={handleSaveToOctarine} appVersion={typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'} - agentInstructionsEnabled={isApiMode && !archive.archiveMode && !annotateMode && !goalSetupMode} + agentInstructionsEnabled={isApiMode && !archive.archiveMode && !annotateMode} obsidianConfigured={isObsidianConfigured()} bearConfigured={getBearSettings().enabled} octarineConfigured={isOctarineConfigured()} @@ -1759,7 +1692,7 @@ const App: React.FC = () => { {/* Tater sprites — inside content wrapper so z-0 stacking context applies */} {taterMode && } {/* Left Sidebar: collapsed tab flags (when sidebar is closed) */} - {wideModeType === null && !sidebar.isOpen && !goalSetupMode && ( + {wideModeType === null && !sidebar.isOpen && ( { )} {/* Left Sidebar: open state (TOC or Version Browser) */} - {sidebar.isOpen && !goalSetupMode && ( + {sidebar.isOpen && ( <> { isSelectingVersion={planDiff.isSelectingVersion} fetchingVersion={planDiff.fetchingVersion} onFetchVersions={planDiff.fetchVersions} - showArchiveTab={isApiMode && !annotateMode && !goalSetupMode} + showArchiveTab={isApiMode && !annotateMode} archivePlans={archive.plans} selectedArchiveFile={archive.selectedFile} onArchiveSelect={archive.select} @@ -1822,7 +1755,7 @@ const App: React.FC = () => { {/* Document Area */} @@ -1843,7 +1776,7 @@ const App: React.FC = () => { truth there. Hidden in plan diff or archive mode, or when sticky actions are disabled. remountToken re-anchors the ResizeObserver when Viewer swaps content (linked docs). */} - {!goalSetupMode && !isPlanDiffActive && !archive.archiveMode && uiPrefs.stickyActionsEnabled && ( + {!isPlanDiffActive && !archive.archiveMode && uiPrefs.stickyActionsEnabled && ( { )} {/* Annotation Toolstrip (hidden during plan diff and archive mode) */} - {!goalSetupMode && !isPlanDiffActive && !archive.archiveMode && ( + {!isPlanDiffActive && !archive.archiveMode && (
{ )} {/* Plan Diff View — rendered when diff data exists, hidden when inactive */} - {goalSetupBundle && ( -
- setSubmitted('approved')} - /> -
- )} - - {planDiff.diffBlocks && planDiff.diffStats && !goalSetupMode && ( + {planDiff.diffBlocks && planDiff.diffStats && (
{
)} {/* Folder annotation empty state — shown before user picks a file */} - {annotateSource === 'folder' && !markdown && !linkedDocHook.isActive && !goalSetupMode && ( + {annotateSource === 'folder' && !markdown && !linkedDocHook.isActive && (

Select a file to annotate

@@ -1917,7 +1838,7 @@ const App: React.FC = () => {
)} {/* Normal Plan View — always mounted, hidden during diff mode */} -
+
{canUseWideMode && !isPlanDiffActive && !archive.archiveMode && (
{ {/* Resize Handle */} - {isPanelOpen && wideModeType === null && !goalSetupMode && } + {isPanelOpen && wideModeType === null && } {/* Annotation Panel */} { title={ archive.archiveMode ? 'Archive Closed' : submitted === 'exited' ? 'Session Closed' - : goalSetupMode ? 'Answers Submitted' : submitted === 'approved' ? (annotateMode ? 'Approved' : 'Plan Approved') : annotateMode ? 'Annotations Sent' @@ -2201,8 +2121,6 @@ const App: React.FC = () => { ? 'Annotation session closed without feedback.' : archive.archiveMode ? 'You can reopen with plannotator archive.' - : goalSetupMode - ? `${agentName} will use your answers to continue.` : submitted === 'approved' ? (annotateMode ? `${agentName} will proceed.` diff --git a/packages/editor/components/AppHeader.tsx b/packages/editor/components/AppHeader.tsx index 45a5ba477..ad3ca9b74 100644 --- a/packages/editor/components/AppHeader.tsx +++ b/packages/editor/components/AppHeader.tsx @@ -13,10 +13,6 @@ interface AppHeaderProps { isApiMode: boolean; annotateMode: boolean; archiveMode: boolean; - goalSetupMode: boolean; - goalSetupCanSubmit: boolean; - goalSetupIsSubmitting: boolean; - goalSetupSubmitLabel: string; gate: boolean; isSharedSession: boolean; origin: Origin | null; @@ -45,8 +41,6 @@ interface AppHeaderProps { onCallbackFeedback: () => void; onCallbackApprove: () => void; onAnnotateExit: () => void; - onGoalSetupExit: () => void; - onGoalSetupSubmit: () => void; onAnnotateFeedback: () => void; onAnnotateApprove: () => void; onFeedback: () => void; @@ -81,10 +75,6 @@ export const AppHeader = React.memo(({ isApiMode, annotateMode, archiveMode, - goalSetupMode, - goalSetupCanSubmit, - goalSetupIsSubmitting, - goalSetupSubmitLabel, gate, isSharedSession, origin, @@ -105,8 +95,6 @@ export const AppHeader = React.memo(({ onCallbackFeedback, onCallbackApprove, onAnnotateExit, - onGoalSetupExit, - onGoalSetupSubmit, onAnnotateFeedback, onAnnotateApprove, onFeedback, @@ -180,28 +168,7 @@ export const AppHeader = React.memo(({ )} - {isApiMode && !linkedDocIsActive && goalSetupMode && ( - <> - - -
- - )} - - {isApiMode && (!linkedDocIsActive || annotateMode) && !archiveMode && !goalSetupMode && ( + {isApiMode && (!linkedDocIsActive || annotateMode) && !archiveMode && ( <> {annotateMode ? ( <> @@ -263,21 +230,19 @@ export const AppHeader = React.memo(({ )} {/* Annotations panel toggle */} - {!goalSetupMode && ( - - )} + {/* Settings dialog (controlled, button hidden — opened from PlanHeaderMenu) */}
@@ -308,9 +273,9 @@ export const AppHeader = React.memo(({ sharingEnabled={canShareCurrentSession} isApiMode={isApiMode} agentInstructionsEnabled={agentInstructionsEnabled} - obsidianConfigured={!goalSetupMode && obsidianConfigured} - bearConfigured={!goalSetupMode && bearConfigured} - octarineConfigured={!goalSetupMode && octarineConfigured} + obsidianConfigured={obsidianConfigured} + bearConfigured={bearConfigured} + octarineConfigured={octarineConfigured} />
diff --git a/packages/editor/shortcuts.ts b/packages/editor/shortcuts.ts index cad6d008b..4b9347aec 100644 --- a/packages/editor/shortcuts.ts +++ b/packages/editor/shortcuts.ts @@ -5,7 +5,6 @@ import { createShortcutRegistry, createShortcutScopeHook, defineShortcutScope, - goalSetupShortcuts, imageAnnotatorShortcuts, inputMethodShortcuts, viewerShortcuts, @@ -108,29 +107,3 @@ export const annotateSurface: ShortcutSurface = { description: 'Shortcuts surfaced by the standalone annotation UI.', registry: annotateSettingsShortcutRegistry, }; - -const goalSetupEditorSettingsShortcuts = defineShortcutScope({ - id: 'goal-setup-editor-settings', - title: 'Goal Setup', - shortcuts: { - submitGoalSetup: { - description: 'Submit answers / facts', - bindings: ['Mod+Enter'], - section: 'Actions', - hint: 'Submits the bundled interview or facts review.', - displayOrder: 10, - }, - }, -}); - -export const goalSetupSettingsShortcutRegistry = createShortcutRegistry([ - goalSetupEditorSettingsShortcuts, - goalSetupShortcuts, -] as const); - -export const goalSetupSurface: ShortcutSurface = { - slug: 'goal-setup', - title: 'Goal setup', - description: 'Shortcuts surfaced by the bundled goal-setup interview and facts review.', - registry: goalSetupSettingsShortcutRegistry, -}; diff --git a/packages/server/package.json b/packages/server/package.json index d5f894661..928814a1b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@plannotator/server", - "version": "0.19.18", + "version": "0.19.17", "private": true, "description": "Shared server implementation for Plannotator plugins", "main": "index.ts", @@ -11,7 +11,6 @@ "./annotate": "./annotate.ts", "./remote": "./remote.ts", "./browser": "./browser.ts", - "./goal-setup": "./goal-setup.ts", "./storage": "./storage.ts", "./git": "./git.ts", "./p4": "./p4.ts", @@ -19,6 +18,14 @@ "./repo": "./repo.ts", "./share-url": "./share-url.ts", "./sessions": "./sessions.ts", + "./session-handler": "./session-handler.ts", + "./daemon/state": "./daemon/state.ts", + "./daemon/session-store": "./daemon/session-store.ts", + "./daemon/server": "./daemon/server.ts", + "./daemon/client": "./daemon/client.ts", + "./daemon/runtime": "./daemon/runtime.ts", + "./daemon/session-factory": "./daemon/session-factory.ts", + "./daemon/start-command": "./daemon/start-command.ts", "./project": "./project.ts", "./pr": "./pr.ts" }, diff --git a/packages/server/sessions.ts b/packages/server/sessions.ts index f703fe844..9a109cc11 100644 --- a/packages/server/sessions.ts +++ b/packages/server/sessions.ts @@ -20,7 +20,7 @@ export interface SessionInfo { pid: number; port: number; url: string; - mode: "plan" | "review" | "annotate" | "archive" | "goal-setup"; + mode: "plan" | "review" | "annotate" | "archive"; project: string; startedAt: string; label: string; diff --git a/packages/ui/components/CommentPopover.tsx b/packages/ui/components/CommentPopover.tsx index c1307910a..d56e50f6b 100644 --- a/packages/ui/components/CommentPopover.tsx +++ b/packages/ui/components/CommentPopover.tsx @@ -18,16 +18,10 @@ interface CommentPopoverProps { initialText?: string; /** Called on submit with comment text and optional images */ onSubmit: (text: string, images?: ImageAttachment[]) => void; - /** Optional live draft observer for submit paths outside the popover. */ - onDraftChange?: (text: string, images?: ImageAttachment[]) => void; /** Called when popover is closed/cancelled */ onClose: () => void; /** Opt-in: persist text + images across close/reopen, keyed by this string. Cleared on submit. */ draftKey?: string; - /** Whether image attachments are available in this comment surface. */ - allowImages?: boolean; - /** Whether submitting empty text is allowed, for editors that support clearing. */ - allowEmptySubmit?: boolean; } const MAX_POPOVER_WIDTH = 384; @@ -70,32 +64,19 @@ export const CommentPopover: React.FC = ({ isGlobal, initialText = '', onSubmit, - onDraftChange, onClose, draftKey, - allowImages = true, - allowEmptySubmit = false, }) => { const [mode, setMode] = useState<'popover' | 'dialog'>('popover'); const initialDraft = draftKey ? draftStore.get(draftKey) : undefined; const [text, setText] = useState(initialDraft?.text ?? initialText); - const [images, setImages] = useState(allowImages ? initialDraft?.images ?? [] : []); + const [images, setImages] = useState(initialDraft?.images ?? []); const [position, setPosition] = useState<{ top: number; left: number; flipAbove: boolean; width: number } | null>(null); const textareaRef = useRef(null); const popoverRef = useRef(null); const { dragPosition, dragHandleProps, wasDragged, reset: resetDrag } = useDraggable(popoverRef); - useEffect(() => { - const nextDraft = draftKey ? draftStore.get(draftKey) : undefined; - setText(nextDraft?.text ?? initialText); - setImages(allowImages ? nextDraft?.images ?? [] : []); - }, [draftKey, initialText, allowImages]); - - useCommentDraftSync(draftKey, text, allowImages ? images : []); - - useEffect(() => { - onDraftChange?.(text, allowImages ? images : undefined); - }, [allowImages, images, onDraftChange, text]); + useCommentDraftSync(draftKey, text, images); // Reset drag when anchor changes (new annotation) or mode switches useEffect(() => { resetDrag(); }, [anchorEl, anchorRect, resetDrag]); @@ -150,12 +131,11 @@ export const CommentPopover: React.FC = ({ }, [mode, onClose]); const handleSubmit = useCallback(() => { - const canSubmitEmpty = allowEmptySubmit && initialText.trim().length > 0; - if (text.trim() || (allowImages && images.length > 0) || canSubmitEmpty) { + if (text.trim() || images.length > 0) { if (draftKey) draftStore.delete(draftKey); - onSubmit(text, allowImages && images.length > 0 ? images : undefined); + onSubmit(text, images.length > 0 ? images : undefined); } - }, [text, images, onSubmit, draftKey, allowImages, allowEmptySubmit, initialText]); + }, [text, images, onSubmit, draftKey]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { @@ -179,10 +159,7 @@ export const CommentPopover: React.FC = ({ ? `"${contextText.length > 50 ? contextText.slice(0, 50) + '...' : contextText}"` : 'Comment'; - const canSubmit = - text.trim().length > 0 || - (allowImages && images.length > 0) || - (allowEmptySubmit && initialText.trim().length > 0); + const canSubmit = text.trim().length > 0 || images.length > 0; if (mode === 'dialog') { return createPortal( @@ -245,14 +222,12 @@ export const CommentPopover: React.FC = ({ {/* Footer */}
- {allowImages && ( - setImages((prev) => [...prev, img])} - onRemove={(path) => setImages((prev) => prev.filter((i) => i.path !== path))} - variant="inline" - /> - )} + setImages((prev) => [...prev, img])} + onRemove={(path) => setImages((prev) => prev.filter((i) => i.path !== path))} + variant="inline" + />
{submitHint} @@ -343,14 +318,12 @@ export const CommentPopover: React.FC = ({ {/* Footer */}
- {allowImages && ( - setImages((prev) => [...prev, img])} - onRemove={(path) => setImages((prev) => prev.filter((i) => i.path !== path))} - variant="inline" - /> - )} + setImages((prev) => [...prev, img])} + onRemove={(path) => setImages((prev) => prev.filter((i) => i.path !== path))} + variant="inline" + />
{submitHint} diff --git a/packages/ui/components/ConfirmDialog.tsx b/packages/ui/components/ConfirmDialog.tsx index e18158e65..dafb88c57 100644 --- a/packages/ui/components/ConfirmDialog.tsx +++ b/packages/ui/components/ConfirmDialog.tsx @@ -2,7 +2,7 @@ * Reusable confirmation dialog component */ -import React, { useEffect } from 'react'; +import React from 'react'; export interface ConfirmDialogProps { isOpen: boolean; @@ -31,25 +31,6 @@ export const ConfirmDialog: React.FC = ({ showCancel = false, wide = false, }) => { - useEffect(() => { - if (!isOpen) return; - const handleKey = (event: KeyboardEvent) => { - if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - event.stopPropagation(); - if (onConfirm) onConfirm(); - else onClose(); - } - if (event.key === 'Escape') { - event.preventDefault(); - event.stopPropagation(); - onClose(); - } - }; - window.addEventListener('keydown', handleKey); - return () => window.removeEventListener('keydown', handleKey); - }, [isOpen, onConfirm, onClose]); - if (!isOpen) return null; const iconColors = { @@ -76,15 +57,8 @@ export const ConfirmDialog: React.FC = ({ }; return ( -
-
+
+
{icons[variant]} diff --git a/packages/ui/components/core/button.tsx b/packages/ui/components/core/button.tsx deleted file mode 100644 index bed67bb2f..000000000 --- a/packages/ui/components/core/button.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; - -type ButtonVariant = 'primary' | 'outline' | 'ghost' | 'icon' | 'danger'; -type ButtonSize = 'sm' | 'md' | 'icon'; - -export interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: ButtonVariant; - size?: ButtonSize; - active?: boolean; -} - -function cx(...classes: Array): string { - return classes.filter(Boolean).join(' '); -} - -export const Button = React.forwardRef(({ - className, - variant = 'outline', - size = 'md', - active = false, - type = 'button', - ...props -}, ref) => ( -