diff --git a/src/adapters/interfaces/ICodeRepository.ts b/src/adapters/interfaces/ICodeRepository.ts index 3e76570..a1845e0 100644 --- a/src/adapters/interfaces/ICodeRepository.ts +++ b/src/adapters/interfaces/ICodeRepository.ts @@ -10,4 +10,5 @@ export interface CodeDiscussion extends Snippet { export interface ICodeRepository { getDiscussionsByFile(file_path: string, remote_url: string, team_id: string): Promise; + subscribeToCodeSnippets(teamId: string, userId: string, callback: (snippet: CodeDiscussion) => void): Promise<{ unsubscribe: () => void }>; } diff --git a/src/adapters/supabase/SupabaseCodeRepository.ts b/src/adapters/supabase/SupabaseCodeRepository.ts index 83f6ec3..ffd6615 100644 --- a/src/adapters/supabase/SupabaseCodeRepository.ts +++ b/src/adapters/supabase/SupabaseCodeRepository.ts @@ -1,6 +1,6 @@ import { SupabaseClient } from "./SupabaseClient"; import { logger } from "../../core/utils/logger"; -import { ICodeRepository, CodeDiscussion} from "../interfaces/ICodeRepository"; +import { ICodeRepository, CodeDiscussion } from "../interfaces/ICodeRepository"; export class SupabaseCodeRepository implements ICodeRepository { async getDiscussionsByFile(file_path: string, remote_url: string, teamId: string): Promise { @@ -24,10 +24,59 @@ export class SupabaseCodeRepository implements ICodeRepository { } if (response.status === 'success') { - logger.info("SupabaseCodeRepository", `Discussions retrieved successfully: ${JSON.stringify(response.discussions)}`); + logger.info("SupabaseCodeRepository", "Code discussions retrieved successfully"); return response.discussions; } throw new Error(`Unexpected response status: ${response.status}`); } + + async subscribeToCodeSnippets(teamId: string, userId: string, callback: (snippet: CodeDiscussion) => void): Promise<{ unsubscribe: () => void }> { + logger.info("SupabaseCodeRepository", `Subscribing to code snippets for team: ${teamId}`); + const supabaseClient = SupabaseClient.getInstance(); + await supabaseClient.syncRealtimeAuth(); + const supabase = supabaseClient.client; + + const channel = supabase + .channel(`code_snippets-team-${teamId}`) + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'code_snippets', + filter: `team_id=eq.${teamId}` + }, + async (payload) => { + logger.info("SupabaseCodeRepository", 'Realtime snippet payload received', payload); + + if (!payload.new || !payload.new.message_id) { + logger.warn("SupabaseCodeRepository", "Payload missing message_id"); + return; + } + + try { + const discussions = await this.getDiscussionsByFile(payload.new.file_path, payload.new.remote_url, teamId); + const newDiscussion = discussions.find(d => d.id === payload.new.id); + if (newDiscussion) { + callback(newDiscussion); + } else { + logger.warn("SupabaseCodeRepository", `New snippet discussion not found after fetch: ${payload.new.id}`); + } + } catch (error) { + logger.error("SupabaseCodeRepository", "Failed to fetch discussion details for snippet", error); + } + } + ) + .subscribe((status) => { + logger.info("SupabaseCodeRepository", `Subscription status for team ${teamId} snippets: ${status}`); + }); + + return { + unsubscribe: () => { + logger.info("SupabaseCodeRepository", `Unsubscribing from code snippets for team: ${teamId}`); + channel.unsubscribe(); + } + }; + } } diff --git a/src/adapters/supabase/SupabaseMessageRepository.ts b/src/adapters/supabase/SupabaseMessageRepository.ts index 2515dcf..9bf58ec 100644 --- a/src/adapters/supabase/SupabaseMessageRepository.ts +++ b/src/adapters/supabase/SupabaseMessageRepository.ts @@ -12,6 +12,7 @@ export class SupabaseMessageRepository implements IMessageRepository { p_team_id: teamId, p_content: message.content, p_attachments: message.attachments, + p_quoted_id: message.quoted_id, p_parent_id: null, }); if (error) { diff --git a/src/core/commands/CLensCommand.ts b/src/core/commands/CLensCommand.ts index 9a6d654..e314bbb 100644 --- a/src/core/commands/CLensCommand.ts +++ b/src/core/commands/CLensCommand.ts @@ -60,12 +60,23 @@ const applyPatch = async (content: string, patch: string, filePath: string): Pro } }; -export const showDiffCommand = async (args: any) => { +export const showDiffCommand = async (diffReference: any) => { try { + let diffArgs; + + if (diffReference?.filePath && diffReference?.discussion_id) { + diffArgs = Container.get('ContextLensService').getDiffArgs(diffReference.filePath, diffReference.discussion_id); + } + + if (!diffArgs) { + logger.error("CLensCommand", "Diff arguments not found"); + return; + } + const { originalContent, currentFileUri, startLine, endLine, commit_sha, filePath, patch, remoteUrl, - liveStartLine, liveEndLine } = args; + liveStartLine, liveEndLine } = diffArgs; if (!originalContent || !currentFileUri || startLine === undefined || endLine === undefined) { logger.error("CLensCommand", "Missing arguments"); diff --git a/src/core/services/ContextLensService.ts b/src/core/services/ContextLensService.ts index d93133e..a5bf934 100644 --- a/src/core/services/ContextLensService.ts +++ b/src/core/services/ContextLensService.ts @@ -36,6 +36,7 @@ export class ContextLensService { allowStale: false }); private relocator = new RelocatorEngine(); + private snippetSubscription?: { unsubscribe: () => void }; constructor(private codeRepo: ICodeRepository, context: vscode.ExtensionContext) { this.buzzDecorationType = vscode.window.createTextEditorDecorationType({ @@ -47,6 +48,9 @@ export class ContextLensService { context.subscriptions.push( vscode.workspace.onDidChangeTextDocument(e => this.updateLiveRanges(e)) ); + if (this._isCLensActive) { + this.startSnippetSubscription(); + } } public toggleCodeLens(value: boolean) { @@ -55,6 +59,9 @@ export class ContextLensService { vscode.window.visibleTextEditors.forEach(editor => { editor.setDecorations(this.buzzDecorationType, []); }); + this.stopSnippetSubscription(); + } else { + this.startSnippetSubscription(); } this._isCLensActive = value; Storage.setGlobal("clens.active", value); @@ -131,6 +138,26 @@ export class ContextLensService { editor.setDecorations(this.buzzDecorationType, decorations); } + public getDiffArgs(uriStr: string, discussionId: string): any | undefined { + const trackedDiscussions = this.cache.get(uriStr); + const td = trackedDiscussions?.find(t => t.discussion.id === discussionId); + if (!td) return undefined; + + return { + originalContent: td.discussion.content, + currentFileUri: uriStr, + startLine: td.discussion.start_line, + endLine: td.discussion.end_line, + liveStartLine: td.liveRange.start.line, + liveEndLine: td.liveRange.end.line, + ref: td.discussion.ref, + commit_sha: td.discussion.commit_sha, + patch: td.discussion.patch, + filePath: td.discussion.file_path, + remoteUrl: td.discussion.remote_url + }; + } + private createMarkdownPopup(discussionList: TrackedDiscussion[], uri: vscode.Uri): vscode.MarkdownString { const md = new vscode.MarkdownString('', true); md.isTrusted = true; @@ -168,20 +195,11 @@ export class ContextLensService { md.appendMarkdown(`$(search-fuzzy) **Partial match:** Review the diff to see the changes.\n\n`); } - const diffArgs = encodeURIComponent(JSON.stringify({ - originalContent: d.discussion.content, - currentFileUri: uri.toString(), - startLine: d.discussion.start_line, - endLine: d.discussion.end_line, - liveStartLine: d.liveRange.start.line, - liveEndLine: d.liveRange.end.line, - ref: d.discussion.ref, - commit_sha: d.discussion.commit_sha, - patch: d.discussion.patch, - filePath: d.discussion.file_path, - remoteUrl: d.discussion.remote_url - })); - md.appendMarkdown(`[$(git-compare)](command:clens.showDiff?${diffArgs} "View Diff")`); + const diffReference = { + filePath: uri.toString(), + discussion_id: d.discussion.id + }; + md.appendMarkdown(`[$(git-compare)](command:clens.showDiff?${encodeURIComponent(JSON.stringify(diffReference))} "View Diff")`); md.appendMarkdown(`  |  `); md.appendMarkdown(`[$(comment-discussion) Jump to Chat](command:linebuzz.jumpToMessage?${encodeURIComponent(JSON.stringify(d.discussion.message.message_id))} "View Discussion")`); @@ -377,8 +395,54 @@ export class ContextLensService { }; } + private async startSnippetSubscription() { + if (this.snippetSubscription) return; + + const teamService = Container.get("TeamService"); + const authService = Container.get("AuthService"); + const team = teamService.getTeam(); + const session = await authService.getSession(); + + if (!team || !session) return; + + this.snippetSubscription = await this.codeRepo.subscribeToCodeSnippets(team.id, session.user_id, (discussion) => { + this.handleNewSnippet(discussion); + }); + } + + private stopSnippetSubscription() { + if (this.snippetSubscription) { + this.snippetSubscription.unsubscribe(); + this.snippetSubscription = undefined; + } + } + + private handleNewSnippet(discussion: CodeDiscussion) { + for (const editor of vscode.window.visibleTextEditors) { + const uriStr = editor.document.uri.toString(); + if (this.cache.has(uriStr)) { + this.getFileContext(editor.document).then(context => { + if (context && context.file_path === discussion.file_path && context.remote_url === discussion.remote_url) { + const trackedDiscussions = this.cache.get(uriStr) || []; + if (trackedDiscussions.some(td => td.discussion.id === discussion.id)) { + return; + } + + const [newTrackedDiscussion] = this.alignDiscussions(editor.document, [discussion]); + if (newTrackedDiscussion) { + trackedDiscussions.push(newTrackedDiscussion); + this.cache.set(uriStr, trackedDiscussions); + vscode.commands.executeCommand('linebuzz.refreshCLens'); + } + } + }); + } + } + } + public dispose() { this.cache.clear(); this.buzzDecorationType.dispose(); + this.stopSnippetSubscription(); } }