Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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/hook/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default defineConfig({
'@plannotator/ui': path.resolve(__dirname, '../../packages/ui'),
'@plannotator/editor/styles': path.resolve(__dirname, '../../packages/editor/index.css'),
'@plannotator/editor': path.resolve(__dirname, '../../packages/editor/App.tsx'),
'@plannotator/shared': path.resolve(__dirname, '../../packages/shared'),
}
},
build: {
Expand Down
10 changes: 3 additions & 7 deletions apps/pi-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,6 @@ export default function plannotator(pi: ExtensionAPI): void {
// Accept path as argument: /plannotator plans/auth.md
let targetPath = args?.trim() || undefined;

// No arg — prompt for file path interactively
if (!targetPath && ctx.hasUI) {
targetPath = await ctx.ui.input("Plan file path", planFilePath);
if (targetPath === undefined) return; // cancelled
}

if (targetPath) planFilePath = targetPath;
enterPlanning(ctx);
},
Expand Down Expand Up @@ -275,7 +269,9 @@ export default function plannotator(pi: ExtensionAPI): void {
return;
}

const absolutePath = resolve(ctx.cwd, filePath);
// Strip leading @ (Pi file reference syntax)
const cleanPath = filePath.startsWith("@") ? filePath.slice(1) : filePath;
const absolutePath = resolve(ctx.cwd, cleanPath);
if (!existsSync(absolutePath)) {
ctx.ui.notify(`File not found: ${absolutePath}`, "error");
return;
Expand Down
1 change: 1 addition & 0 deletions apps/portal/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default defineConfig({
'@plannotator/ui': path.resolve(__dirname, '../../packages/ui'),
'@plannotator/editor/styles': path.resolve(__dirname, '../../packages/editor/index.css'),
'@plannotator/editor': path.resolve(__dirname, '../../packages/editor/App.tsx'),
'@plannotator/shared': path.resolve(__dirname, '../../packages/shared'),
}
},
build: {
Expand Down
1 change: 1 addition & 0 deletions apps/review/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
'@plannotator/ui': path.resolve(__dirname, '../../packages/ui'),
'@plannotator/review-editor/styles': path.resolve(__dirname, '../../packages/review-editor/index.css'),
'@plannotator/review-editor': path.resolve(__dirname, '../../packages/review-editor/App.tsx'),
'@plannotator/shared': path.resolve(__dirname, '../../packages/shared'),
}
},
build: {
Expand Down
7 changes: 4 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 23 additions & 63 deletions packages/review-editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ const ReviewApp: React.FC = () => {
const [isSendingFeedback, setIsSendingFeedback] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const [submitted, setSubmitted] = useState<'approved' | 'feedback' | false>(false);
const [showApproveWarning, setShowApproveWarning] = useState(false);
const [sharingEnabled, setSharingEnabled] = useState(true);
const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null);

Expand Down Expand Up @@ -554,7 +553,7 @@ const ReviewApp: React.FC = () => {

const tag = (e.target as HTMLElement)?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (showExportModal || showNoAnnotationsDialog || showApproveWarning) return;
if (showExportModal || showNoAnnotationsDialog) return;
if (submitted || isSendingFeedback || isApproving) return;
if (!origin) return; // Demo mode

Expand All @@ -571,7 +570,7 @@ const ReviewApp: React.FC = () => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
showExportModal, showNoAnnotationsDialog, showApproveWarning,
showExportModal, showNoAnnotationsDialog,
submitted, isSendingFeedback, isApproving, origin, totalAnnotationCount,
handleApprove, handleSendFeedback
]);
Expand Down Expand Up @@ -682,56 +681,33 @@ const ReviewApp: React.FC = () => {

{origin ? (
<>
{/* Send Feedback button - accent color, disabled if no annotations */}
{/* Single action button: Send Feedback (if annotations) or Approve (if none) */}
<button
onClick={handleSendFeedback}
disabled={isSendingFeedback || isApproving || totalAnnotationCount === 0}
className={`p-1.5 md:px-2.5 md:py-1 rounded-md text-xs font-medium transition-all ${
onClick={totalAnnotationCount > 0 ? handleSendFeedback : handleApprove}
disabled={isSendingFeedback || isApproving}
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
isSendingFeedback || isApproving
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
: totalAnnotationCount === 0
? 'opacity-50 cursor-not-allowed bg-accent/10 text-accent/50'
: 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/30'
: totalAnnotationCount > 0
? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/30'
: 'bg-success text-success-foreground hover:opacity-90'
}`}
title={totalAnnotationCount === 0 ? "Add annotations to send feedback" : "Send feedback"}
title={totalAnnotationCount > 0 ? "Send feedback" : "Approve - no changes needed"}
>
<svg className="w-4 h-4 md:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span className="hidden md:inline">{isSendingFeedback ? 'Sending...' : 'Send Feedback'}</span>
</button>

{/* Approve button - green/success, dimmed if annotations exist */}
<div className="relative group/approve">
<button
onClick={() => {
if (totalAnnotationCount > 0) {
setShowApproveWarning(true);
} else {
handleApprove();
}
}}
disabled={isSendingFeedback || isApproving}
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
isSendingFeedback || isApproving
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
: totalAnnotationCount > 0
? 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground'
: 'bg-success text-success-foreground hover:opacity-90'
}`}
title="Approve - no changes needed"
>
<span className="md:hidden">{isApproving ? '...' : 'OK'}</span>
<span className="hidden md:inline">{isApproving ? 'Approving...' : 'Approve'}</span>
</button>
{totalAnnotationCount > 0 && (
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-56 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50">
<div className="absolute bottom-full right-4 border-4 border-transparent border-b-border" />
<div className="absolute bottom-full right-4 mt-px border-4 border-transparent border-b-popover" />
Your {totalAnnotationCount} annotation{totalAnnotationCount !== 1 ? 's' : ''} won't be sent if you approve.
</div>
{totalAnnotationCount > 0 ? (
<>
<svg className="w-4 h-4 md:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span className="hidden md:inline">{isSendingFeedback ? 'Sending...' : 'Send Feedback'}</span>
</>
) : (
<>
<span className="md:hidden">{isApproving ? '...' : 'OK'}</span>
<span className="hidden md:inline">{isApproving ? 'Approving...' : 'Approve'}</span>
</>
)}
</div>
</button>
</>
) : (
<button
Expand Down Expand Up @@ -970,22 +946,6 @@ const ReviewApp: React.FC = () => {
variant="info"
/>

{/* Approve with annotations warning */}
<ConfirmDialog
isOpen={showApproveWarning}
onClose={() => setShowApproveWarning(false)}
onConfirm={() => {
setShowApproveWarning(false);
handleApprove();
}}
title="Annotations Won't Be Sent"
message={<>You have {totalAnnotationCount} annotation{totalAnnotationCount !== 1 ? 's' : ''} that will be lost if you approve.</>}
subMessage="To send your feedback, use Send Feedback instead."
confirmText="Approve Anyway"
cancelText="Cancel"
variant="warning"
showCancel
/>

{/* Completion overlay - shown after approve/feedback */}
<CompletionOverlay
Expand Down
20 changes: 20 additions & 0 deletions packages/server/resolve-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,26 @@ describe("resolveMarkdownFile", () => {
});
});

// @ prefix stripping (coding agent file references)

test("strips leading @ from relative path", async () => {
const root = createTempProject({ "docs/analyse.md": "# Analyse" });
const result = await resolveMarkdownFile("@docs/analyse.md", root);
expect(result).toEqual({
kind: "found",
path: resolve(root, "docs/analyse.md"),
});
});

test("strips leading @ from bare filename", async () => {
const root = createTempProject({ "plan.md": "# Plan" });
const result = await resolveMarkdownFile("@plan.md", root);
expect(result).toEqual({
kind: "found",
path: resolve(root, "plan.md"),
});
});

// Edge cases

test("returns not_found for nonexistent file", async () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/server/resolve-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export async function resolveMarkdownFile(
): Promise<ResolveResult> {
// Trim whitespace/CR that may leak from Windows shell pipelines
input = input.trim();
// Strip leading @ (coding agent file reference syntax, e.g. @docs/file.md)
if (input.startsWith("@")) {
input = input.slice(1);
}
const normalizedInput = normalizeMarkdownPathInput(input);
const searchInput = normalizeSeparators(normalizedInput);
const isBareFilename = !searchInput.includes("/");
Expand Down
Loading