mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
a278fdaf77
* feat: plan archive browser with in-session sidebar tab (#362) Add `plannotator archive` subcommand and archive sidebar tab for browsing saved plan decisions from ~/.plannotator/plans/. Plans show approved/denied badges and timestamps. In-session mode uses the linked doc overlay pattern so users can reference old plans while reviewing a current one. - New archive server (packages/server/archive.ts) following annotate pattern - New ArchiveBrowser sidebar component, reusable in both contexts - Archive listing/parsing functions in storage.ts (reads decision snapshots) - Archive endpoints on plan server for in-session use (/api/archive/plans) - Remove dead "Other Plans" UI, projectPlans state, /api/plan/history endpoint - Fix resize handle touch area covering scrollbars in sidebar/main content - Fix sidebar tab bar overflow when narrow For provenance purposes, this commit was AI assisted. * fix: code quality sweep for plan archive - Remove `as any` cast: add "archive" to SessionInfo.mode union - Replace inline import() type with proper import for ArchivedPlan - Replace any[] with ArchivedPlan[] in fetch response types - Fix infinite re-fetch when archive is empty (use hasFetched ref) - Cache archive plan list in plan server (avoid re-scanning filesystem) - Document ResizeHandle side prop behavior - Remove redundant comment on Viewer archiveInfo prop For provenance purposes, this commit was AI assisted. * chore: remove dead marketing components Step.astro and Landing.astro are unused — landing page inlines step markup and pages use Base.astro directly. For provenance purposes, this commit was AI assisted. * fix: address code review findings for plan archive - Path traversal: use resolve() + trailing separator guard (matches reference-handlers.ts) - Thread customPath into in-session archive endpoints via query param - Sort same-day archive entries by mtime instead of title - Clear selectedArchiveFile on linked doc back to prevent badge leak - Hide archive tab in annotate mode (server doesn't serve those endpoints) - Add targetTab param to useLinkedDoc.open() to preserve calling sidebar tab - Replace mutable render variable with index-based date grouping For provenance purposes, this commit was AI assisted. * refactor: collapse standalone archive server into plan server Delete packages/server/archive.ts (187 lines) — nearly all duplicated from the plan server. Add mode:"archive" option to startPlannotatorServer instead. Fixes two bugs from code review: - handleArchiveCopy now splits on "# Plan Feedback" marker instead of bare "---", preventing truncation at horizontal rules in plan content - customPath support works in standalone archive mode (was only working in-session because the standalone server never received it) For provenance purposes, this commit was AI assisted. * refactor: extract useArchive hook from App.tsx Move archive state (archiveMode, plans, selectedFile, isLoading) and handlers (select, fetchPlans, done, copy) into a dedicated useArchive hook. Reduces App.tsx by ~75 lines and makes the archive feature self-contained. For provenance purposes, this commit was AI assisted. * feat: Pi archive parity + eliminate server duplication Move runtime-agnostic storage, draft, and project functions from packages/server/ to packages/shared/ — eliminating ~250 lines of duplicated code in Pi's server.ts. Server package becomes thin re-exports, preserving all existing import paths. Add archive mode to Pi's plan review server (mode, routes, waitForDone) and register /plannotator-archive command in the Pi extension. Consolidate ArchivedPlan type to single definition in shared/storage.ts. Simplify archive copy to include full content with feedback. For provenance purposes, this commit was AI assisted. * fix: drop -core suffix from Pi shared copies The -core suffix broke cross-file imports — storage.ts imports from ./project which didn't resolve to project-core.ts. Using the original filenames (no collision) lets relative imports work naturally. For provenance purposes, this commit was AI assisted. * fix: archive custom path bugs, disable sharing, update docs - Normalize planDir via resolve() in getPlanDir() to handle relative paths and trailing slashes in the path traversal guard - Re-fetch archive plans client-side with cookie-backed customPath so standalone archive respects the user's configured save location - Disable sharing in archive mode (read-only viewer, no need) - Remove dead /api/plan/history endpoint and listProjectPlans import from Pi extension - Remove dead /api/plan/history mock from dev-mock-api - Update CLAUDE.md and AGENTS.md: add archive flow, archive API endpoints, shared package structure, correct storage location, sidebar tab count, remove stale /api/plan/history references - Update hook server docstring from four to five modes For provenance purposes, this commit was AI assisted. * fix: empty archive shows demo content, stale viewer after customPath fetch - Clear demo markdown when archive opens with no plans (plan: "" was falsy, so setMarkdown was never called) - Remove redundant fetchPlans() from archive init — server already sends archivePlans in initial response - After fetchPlans() resolves with customPath results, auto-select and load the first plan into the viewer - Remove dead listProjectPlans re-export from server barrel For provenance purposes, this commit was AI assisted. * refactor: gitignore Pi shared copies, add @generated headers Pi's copied .ts files (storage, draft, project, feedback-templates, review-core) are build artifacts generated from packages/shared/. They looked like editable source files, leading to confusion about which file to edit. Now gitignored like the HTML copies, with @generated headers prepended by the build script. For provenance purposes, this commit was AI assisted. * fix: generate Pi shared copies in CI before tests The Pi .ts copies are now gitignored build artifacts. CI needs to generate them before running tests since server.test.ts transitively imports them via server.ts. For provenance purposes, this commit was AI assisted. * fix: generate Pi shared copies in release pipeline test job Same fix as test.yml — the Pi .ts copies are gitignored, so the test job in the release pipeline also needs to generate them before bun test. For provenance purposes, this commit was AI assisted. * fix: use block scalar in CI workflow to avoid YAML parse error The inline `run:` had a colon in the printf string that YAML interpreted as a mapping key. Switch to `run: |` block scalar. For provenance purposes, this commit was AI assisted.
245 lines
6.7 KiB
TypeScript
245 lines
6.7 KiB
TypeScript
/**
|
|
* Vite plugin that mocks plannotator API endpoints for local development.
|
|
* Provides plan data with version history so the Versions tab works in dev mode.
|
|
*/
|
|
import type { Plugin } from 'vite';
|
|
|
|
// Version 1: earlier draft (shorter, missing sections)
|
|
const PLAN_V1 = `# Implementation Plan: Real-time Collaboration
|
|
|
|
## Overview
|
|
Add real-time collaboration features to the editor using WebSocket connections.
|
|
|
|
## Phase 1: Infrastructure
|
|
|
|
### WebSocket Server
|
|
Set up a WebSocket server to handle concurrent connections:
|
|
|
|
\`\`\`typescript
|
|
const server = new WebSocketServer({ port: 8080 });
|
|
|
|
server.on('connection', (socket) => {
|
|
const sessionId = generateSessionId();
|
|
sessions.set(sessionId, socket);
|
|
|
|
socket.on('message', (data) => {
|
|
broadcast(sessionId, data);
|
|
});
|
|
});
|
|
\`\`\`
|
|
|
|
### Client Connection
|
|
- Establish persistent connection on document load
|
|
- Implement reconnection logic with exponential backoff
|
|
- Handle offline state gracefully
|
|
|
|
### Database Schema
|
|
|
|
\`\`\`sql
|
|
CREATE TABLE documents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title VARCHAR(255) NOT NULL,
|
|
content JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
\`\`\`
|
|
|
|
## Phase 2: Operational Transforms
|
|
|
|
> The key insight is that we need to transform operations against concurrent operations to maintain consistency.
|
|
|
|
Key requirements:
|
|
- Transform insert against insert
|
|
- Transform insert against delete
|
|
- Transform delete against delete
|
|
|
|
## Pre-launch Checklist
|
|
|
|
- [ ] Infrastructure ready
|
|
- [ ] WebSocket server deployed
|
|
- [ ] Database migrations applied
|
|
- [ ] Security audit complete
|
|
- [ ] Documentation updated
|
|
|
|
---
|
|
|
|
**Target:** Ship MVP in next sprint
|
|
`;
|
|
|
|
// Version 2: expanded (added architecture diagram, more details)
|
|
const PLAN_V2 = `# Implementation Plan: Real-time Collaboration
|
|
|
|
## Overview
|
|
Add real-time collaboration features to the editor using WebSocket connections and operational transforms.
|
|
|
|
### Architecture
|
|
|
|
\`\`\`mermaid
|
|
flowchart LR
|
|
subgraph Client["Client Browser"]
|
|
UI[React UI] --> OT[OT Engine]
|
|
OT <--> WS[WebSocket Client]
|
|
end
|
|
|
|
subgraph Server["Backend"]
|
|
WSS[WebSocket Server] <--> OTS[OT Transform]
|
|
OTS <--> DB[(PostgreSQL)]
|
|
end
|
|
|
|
WS <--> WSS
|
|
\`\`\`
|
|
|
|
## Phase 1: Infrastructure
|
|
|
|
### WebSocket Server
|
|
Set up a WebSocket server to handle concurrent connections:
|
|
|
|
\`\`\`typescript
|
|
const server = new WebSocketServer({ port: 8080 });
|
|
|
|
server.on('connection', (socket, request) => {
|
|
const sessionId = generateSessionId();
|
|
sessions.set(sessionId, socket);
|
|
|
|
socket.on('message', (data) => {
|
|
broadcast(sessionId, data);
|
|
});
|
|
});
|
|
\`\`\`
|
|
|
|
### Client Connection
|
|
- Establish persistent connection on document load
|
|
- Initialize WebSocket with authentication token
|
|
- Set up heartbeat ping/pong every 30 seconds
|
|
- Implement reconnection logic with exponential backoff
|
|
- Start with 1 second delay
|
|
- Double delay on each retry (max 30 seconds)
|
|
- Handle offline state gracefully
|
|
- Queue local changes in IndexedDB
|
|
- Show offline indicator in UI
|
|
|
|
### Database Schema
|
|
|
|
\`\`\`sql
|
|
CREATE TABLE documents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title VARCHAR(255) NOT NULL,
|
|
content JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE collaborators (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL,
|
|
role VARCHAR(50) DEFAULT 'editor',
|
|
cursor_position JSONB,
|
|
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_collaborators_document ON collaborators(document_id);
|
|
\`\`\`
|
|
|
|
## Phase 2: Operational Transforms
|
|
|
|
> The key insight is that we need to transform operations against concurrent operations to maintain consistency.
|
|
|
|
Key requirements:
|
|
- Transform insert against insert
|
|
- Same position: use user ID for deterministic ordering
|
|
- Different positions: adjust offset of later operation
|
|
- Transform insert against delete
|
|
- Insert before delete: no change needed
|
|
- Insert inside deleted range: special handling required
|
|
- Transform delete against delete
|
|
- Non-overlapping: adjust positions
|
|
- Overlapping: merge or split operations
|
|
- Maintain cursor positions across transforms
|
|
|
|
## Phase 3: UI Updates
|
|
|
|
1. Show collaborator cursors in real-time
|
|
2. Display presence indicators
|
|
3. Add conflict resolution UI
|
|
4. Implement undo/redo stack per user
|
|
|
|
## Pre-launch Checklist
|
|
|
|
- [ ] Infrastructure ready
|
|
- [x] WebSocket server deployed
|
|
- [x] Database migrations applied
|
|
- [ ] Load balancer configured
|
|
- [ ] Security audit complete
|
|
- [x] Authentication flow reviewed
|
|
- [ ] Rate limiting implemented
|
|
- [x] Documentation updated
|
|
|
|
---
|
|
|
|
**Target:** Ship MVP in next sprint
|
|
`;
|
|
|
|
// Version 3 is the current PLAN_CONTENT from App.tsx (loaded by the editor itself)
|
|
// We don't duplicate it here — the editor already has it as the default state.
|
|
|
|
const now = Date.now();
|
|
const versions = [
|
|
{ version: 1, timestamp: new Date(now - 3600_000 * 2).toISOString() },
|
|
{ version: 2, timestamp: new Date(now - 3600_000).toISOString() },
|
|
{ version: 3, timestamp: new Date(now - 60_000).toISOString() },
|
|
];
|
|
|
|
const versionPlans: Record<number, string> = {
|
|
1: PLAN_V1,
|
|
2: PLAN_V2,
|
|
// Version 3 is the current plan — served via /api/plan
|
|
};
|
|
|
|
export function devMockApi(): Plugin {
|
|
return {
|
|
name: 'plannotator-dev-mock-api',
|
|
configureServer(server) {
|
|
server.middlewares.use((req, res, next) => {
|
|
if (req.url === '/api/plan') {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
plan: undefined, // Let editor use its own PLAN_CONTENT
|
|
origin: 'claude-code',
|
|
previousPlan: PLAN_V2,
|
|
versionInfo: { version: 3, totalVersions: 3, project: 'demo' },
|
|
sharingEnabled: true,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (req.url === '/api/plan/versions') {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
project: 'demo',
|
|
slug: 'implementation-plan-real-time-collab',
|
|
versions,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (req.url?.startsWith('/api/plan/version?')) {
|
|
const url = new URL(req.url, 'http://localhost');
|
|
const v = Number(url.searchParams.get('v'));
|
|
const plan = versionPlans[v];
|
|
if (plan) {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ plan, version: v }));
|
|
} else {
|
|
res.statusCode = 404;
|
|
res.end(JSON.stringify({ error: 'Version not found' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
next();
|
|
});
|
|
},
|
|
};
|
|
}
|