@@ -19,6 +19,7 @@ import {
1919 type AssetKind ,
2020 type CodeSurface ,
2121 type Comment ,
22+ type CommentAnchor ,
2223 type DiffSurface ,
2324 htmlSurface ,
2425 type MarkdownSurface ,
@@ -265,6 +266,7 @@ export interface Feedback {
265266 surfaceTitle : string | null ;
266267 text : string ;
267268 at : string ;
269+ anchor ?: CommentAnchor ;
268270}
269271
270272// Lean comment shape attached to agent-facing responses.
@@ -273,6 +275,7 @@ const feedbackView = (c: Comment): Feedback => ({
273275 surfaceTitle : c . postTitle ,
274276 text : c . text ,
275277 at : c . createdAt ,
278+ ...( c . anchor && { anchor : c . anchor } ) ,
276279} ) ;
277280
278281export function createApp ( {
@@ -654,10 +657,65 @@ export function createApp({
654657 return reviseSurface ( id , { parts : indexed } ) ;
655658 }
656659
660+ function numberInRange ( value : unknown , min : number , max : number ) : number | null {
661+ const n = Number ( value ) ;
662+ return Number . isFinite ( n ) && n >= min && n <= max ? n : null ;
663+ }
664+
665+ function sanitizeCommentAnchor ( raw : unknown , post : Post ) : CommentAnchor | undefined {
666+ if ( ! raw || typeof raw !== "object" ) return undefined ;
667+ const input = raw as Record < string , unknown > ;
668+ const kind = input . kind === "rect" || input . kind === "lineRange" ? input . kind : "point" ;
669+ let surfaceIndex = Number ( input . surfaceIndex ) ;
670+ if (
671+ ! Number . isInteger ( surfaceIndex ) ||
672+ surfaceIndex < 0 ||
673+ surfaceIndex >= post . surfaces . length
674+ ) {
675+ const surfaceId = typeof input . surfaceId === "string" ? input . surfaceId : undefined ;
676+ surfaceIndex = post . surfaces . findIndex ( ( s ) => s . id === surfaceId ) ;
677+ }
678+ if ( surfaceIndex < 0 || surfaceIndex >= post . surfaces . length ) return undefined ;
679+ const surface = post . surfaces [ surfaceIndex ] ;
680+ const base = {
681+ surfaceIndex,
682+ ...( surface . id && { surfaceId : surface . id } ) ,
683+ surfaceKind : surface . kind ,
684+ // The server pins anchors to the current stored version instead of trusting
685+ // the client-supplied value.
686+ postVersion : post . version ,
687+ } ;
688+ if ( kind === "lineRange" ) {
689+ const startLine = Number ( input . startLine ) ;
690+ const endLine = Number ( input . endLine ) ;
691+ if ( ! Number . isInteger ( startLine ) || ! Number . isInteger ( endLine ) || startLine < 1 ) {
692+ return undefined ;
693+ }
694+ return {
695+ kind,
696+ ...base ,
697+ startLine,
698+ endLine : Math . max ( startLine , endLine ) ,
699+ ...( typeof input . file === "string" && { file : input . file . slice ( 0 , MAX_TITLE ) } ) ,
700+ } ;
701+ }
702+ const x = numberInRange ( input . x , 0 , 1 ) ;
703+ const y = numberInRange ( input . y , 0 , 1 ) ;
704+ if ( x == null || y == null ) return undefined ;
705+ if ( kind === "rect" ) {
706+ const w = numberInRange ( input . w , 0 , 1 ) ;
707+ const h = numberInRange ( input . h , 0 , 1 ) ;
708+ if ( w == null || h == null ) return undefined ;
709+ return { kind, ...base , x, y, w, h } ;
710+ }
711+ return { kind : "point" , ...base , x, y } ;
712+ }
713+
657714 async function createComment ( input : {
658715 text : string ;
659716 surface ?: string ;
660717 author : string ;
718+ anchor ?: unknown ;
661719 } ) : Promise <
662720 { comment : Comment ; userFeedback ?: Feedback [ ] } | { error : string ; status : 400 | 404 }
663721 > {
@@ -671,6 +729,7 @@ export function createApp({
671729 postId : surface . id ,
672730 author : input . author ,
673731 text : input . text . trim ( ) . slice ( 0 , MAX_COMMENT_TEXT ) ,
732+ anchor : sanitizeCommentAnchor ( input . anchor , surface ) ,
674733 } ) ;
675734 if ( ! comment ) return { error : "session not found" , status : 404 } ;
676735 bus . broadcast ( {
@@ -1274,6 +1333,7 @@ export function createApp({
12741333 text : body . text ,
12751334 surface : typeof surface === "string" ? surface : undefined ,
12761335 author : typeof body . author === "string" ? body . author : "user" ,
1336+ anchor : body . anchor ,
12771337 } ) ;
12781338 if ( "error" in result ) return c . json ( { error : result . error } , result . status ) ;
12791339 return c . json (
@@ -1282,6 +1342,13 @@ export function createApp({
12821342 ) ;
12831343 } ) ;
12841344
1345+ app . delete ( "/api/comments/:id" , async ( c ) => {
1346+ const comment = await store . removeComment ( c . req . param ( "id" ) ) ;
1347+ if ( ! comment ) return c . json ( { error : "comment not found" } , 404 ) ;
1348+ bus . broadcast ( { type : "comment-deleted" , id : comment . id , sessionId : comment . sessionId } ) ;
1349+ return c . json ( { ok : true } ) ;
1350+ } ) ;
1351+
12851352 // The viewer's update notice: running version vs latest published release.
12861353 app . get ( "/api/version" , async ( c ) => {
12871354 if ( ! version ) return c . json ( { current : null , latest : null , updateAvailable : false } ) ;
0 commit comments