@@ -156,6 +156,10 @@ function migrate(db: SqliteDb): void {
156156 status TEXT NOT NULL,
157157 created_at INTEGER NOT NULL,
158158 updated_at INTEGER NOT NULL,
159+ anchor_state TEXT,
160+ anchored_version INTEGER,
161+ author_member_id TEXT,
162+ last_good_position_json TEXT,
159163 UNIQUE(project_id, conversation_id, file_path, element_id, slide_key),
160164 FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
161165 FOREIGN KEY(conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
@@ -329,6 +333,21 @@ function migrate(db: SqliteDb): void {
329333 db . exec ( `ALTER TABLE preview_comments ADD COLUMN slide_index INTEGER` ) ;
330334 }
331335 migratePreviewCommentsSlideKey ( db ) ;
336+ // Team-collab anchor columns — added after the slide-key rebuild so a legacy
337+ // table rebuild cannot drop them.
338+ const previewCommentAnchorCols = db . prepare ( `PRAGMA table_info(preview_comments)` ) . all ( ) as DbRow [ ] ;
339+ if ( ! previewCommentAnchorCols . some ( ( c : DbRow ) => c . name === 'anchor_state' ) ) {
340+ db . exec ( `ALTER TABLE preview_comments ADD COLUMN anchor_state TEXT` ) ;
341+ }
342+ if ( ! previewCommentAnchorCols . some ( ( c : DbRow ) => c . name === 'anchored_version' ) ) {
343+ db . exec ( `ALTER TABLE preview_comments ADD COLUMN anchored_version INTEGER` ) ;
344+ }
345+ if ( ! previewCommentAnchorCols . some ( ( c : DbRow ) => c . name === 'author_member_id' ) ) {
346+ db . exec ( `ALTER TABLE preview_comments ADD COLUMN author_member_id TEXT` ) ;
347+ }
348+ if ( ! previewCommentAnchorCols . some ( ( c : DbRow ) => c . name === 'last_good_position_json' ) ) {
349+ db . exec ( `ALTER TABLE preview_comments ADD COLUMN last_good_position_json TEXT` ) ;
350+ }
332351 const deploymentCols = db . prepare ( `PRAGMA table_info(deployments)` ) . all ( ) as DbRow [ ] ;
333352 if ( ! deploymentCols . some ( ( c : DbRow ) => c . name === 'status' ) ) {
334353 db . exec ( `ALTER TABLE deployments ADD COLUMN status TEXT NOT NULL DEFAULT 'ready'` ) ;
@@ -1610,6 +1629,8 @@ export function listPreviewComments(db: SqliteDb, projectId: string, conversatio
16101629 pod_members_json AS podMembersJson, style_json AS styleJson,
16111630 attachments_json AS attachmentsJson,
16121631 slide_index AS slideIndex,
1632+ anchor_state AS anchorState, anchored_version AS anchoredVersion,
1633+ author_member_id AS authorMemberId, last_good_position_json AS lastGoodPositionJson,
16131634 note, status, created_at AS createdAt, updated_at AS updatedAt
16141635 FROM preview_comments
16151636 WHERE project_id = ? AND conversation_id = ?
@@ -1643,6 +1664,15 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
16431664 : 0 ;
16441665 const slideIndex = Number . isFinite ( target . slideIndex ) ? Math . max ( 0 , Math . round ( target . slideIndex ) ) : null ;
16451666 const slideKey = slideIndex ?? - 1 ;
1667+ // Team-collab creation metadata. anchor_state / last_good_position stay null at
1668+ // creation — the drift ladder resolves and writes them back (updatePreviewCommentAnchor).
1669+ const anchoredVersion = Number . isFinite ( target . anchoredVersion )
1670+ ? Math . max ( 0 , Math . round ( target . anchoredVersion ) )
1671+ : null ;
1672+ const authorMemberId =
1673+ typeof input ?. authorMemberId === 'string' && input . authorMemberId . trim ( )
1674+ ? input . authorMemberId . trim ( )
1675+ : null ;
16461676 const now = Date . now ( ) ;
16471677 const existing = db
16481678 . prepare (
@@ -1661,8 +1691,9 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
16611691 `INSERT INTO preview_comments
16621692 (id, project_id, conversation_id, file_path, element_id, selector, label,
16631693 text, position_json, html_hint, selection_kind, member_count, pod_members_json,
1664- style_json, attachments_json, slide_index, slide_key, note, status, created_at, updated_at)
1665- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1694+ style_json, attachments_json, slide_index, slide_key, note, status, created_at, updated_at,
1695+ anchored_version, author_member_id)
1696+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16661697 ON CONFLICT(project_id, conversation_id, file_path, element_id, slide_key) DO UPDATE SET
16671698 selector = excluded.selector,
16681699 label = excluded.label,
@@ -1677,6 +1708,8 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
16771708 slide_index = excluded.slide_index,
16781709 note = excluded.note,
16791710 status = 'open',
1711+ anchored_version = excluded.anchored_version,
1712+ author_member_id = excluded.author_member_id,
16801713 updated_at = excluded.updated_at` ,
16811714 ) . run (
16821715 id ,
@@ -1700,6 +1733,8 @@ export function upsertPreviewComment(db: SqliteDb, projectId: string, conversati
17001733 'open' ,
17011734 createdAt ,
17021735 now ,
1736+ anchoredVersion ,
1737+ authorMemberId ,
17031738 ) ;
17041739 return getPreviewComment ( db , projectId , conversationId , id ) ;
17051740}
@@ -1715,6 +1750,41 @@ export function updatePreviewCommentStatus(db: SqliteDb, projectId: string, conv
17151750 return getPreviewComment ( db , projectId , conversationId , id ) ;
17161751}
17171752
1753+ /**
1754+ * Team-collab drift-ladder write-back: persist how a comment resolved this render.
1755+ * `lastGoodPosition`/`anchoredVersion` are COALESCEd so a `lost` resolve (which omits
1756+ * them) keeps the last known-good values instead of wiping them. Does not bump
1757+ * `updated_at` — anchor resolution is a derived read, not a content edit.
1758+ */
1759+ export function updatePreviewCommentAnchor (
1760+ db : SqliteDb ,
1761+ projectId : string ,
1762+ conversationId : string ,
1763+ id : string ,
1764+ input : DbRow ,
1765+ ) {
1766+ const anchorState = typeof input ?. anchorState === 'string' ? input . anchorState : null ;
1767+ const lastGoodPosition = input ?. lastGoodPosition ? normalizePosition ( input . lastGoodPosition ) : null ;
1768+ const anchoredVersion = Number . isFinite ( input ?. anchoredVersion )
1769+ ? Math . max ( 0 , Math . round ( input . anchoredVersion ) )
1770+ : null ;
1771+ db . prepare (
1772+ `UPDATE preview_comments
1773+ SET anchor_state = ?,
1774+ last_good_position_json = COALESCE(?, last_good_position_json),
1775+ anchored_version = COALESCE(?, anchored_version)
1776+ WHERE id = ? AND project_id = ? AND conversation_id = ?` ,
1777+ ) . run (
1778+ anchorState ,
1779+ lastGoodPosition ? JSON . stringify ( lastGoodPosition ) : null ,
1780+ anchoredVersion ,
1781+ id ,
1782+ projectId ,
1783+ conversationId ,
1784+ ) ;
1785+ return getPreviewComment ( db , projectId , conversationId , id ) ;
1786+ }
1787+
17181788export function deletePreviewComment ( db : SqliteDb , projectId : string , conversationId : string , id : string ) {
17191789 const result = db
17201790 . prepare (
@@ -1735,6 +1805,8 @@ function getPreviewComment(db: SqliteDb, projectId: string, conversationId: stri
17351805 pod_members_json AS podMembersJson, style_json AS styleJson,
17361806 attachments_json AS attachmentsJson,
17371807 slide_index AS slideIndex,
1808+ anchor_state AS anchorState, anchored_version AS anchoredVersion,
1809+ author_member_id AS authorMemberId, last_good_position_json AS lastGoodPositionJson,
17381810 note, status, created_at AS createdAt, updated_at AS updatedAt
17391811 FROM preview_comments
17401812 WHERE id = ? AND project_id = ? AND conversation_id = ?` ,
@@ -1772,6 +1844,10 @@ function normalizePreviewComment(row: DbRow) {
17721844 status : row . status ,
17731845 createdAt : row . createdAt ,
17741846 updatedAt : row . updatedAt ,
1847+ anchorState : typeof row . anchorState === 'string' ? row . anchorState : undefined ,
1848+ anchoredVersion : Number . isFinite ( row . anchoredVersion ) ? row . anchoredVersion : undefined ,
1849+ authorMemberId : typeof row . authorMemberId === 'string' ? row . authorMemberId : undefined ,
1850+ lastGoodPosition : parseJsonOrUndef ( row . lastGoodPositionJson ) ,
17751851 } ;
17761852}
17771853
0 commit comments