فهرست منبع

fix: address follow-up interview review comments

alvinreal 1 ماه پیش
والد
کامیت
334baddbca
5فایلهای تغییر یافته به همراه76 افزوده شده و 26 حذف شده
  1. 53 5
      src/interview/dashboard.ts
  2. 4 3
      src/interview/document.ts
  3. 2 0
      src/interview/manager.ts
  4. 15 18
      src/interview/service.ts
  5. 2 0
      src/interview/types.ts

+ 53 - 5
src/interview/dashboard.ts

@@ -208,10 +208,43 @@ export function createDashboardServer(config: DashboardConfig): {
   // SSE client registry: interviewId → Set<ServerResponse>
   const sseClients = new Map<string, Set<import('node:http').ServerResponse>>();
 
-  function broadcastSse(interviewId: string, data: unknown) {
+  function formatSseState(entry: InterviewStateEntry) {
+    const markdownPath = entry.filePath;
+    const displayPath = markdownPath
+      ? markdownPath.split('/').pop() || markdownPath
+      : 'interview.md';
+    const document = entry.document ?? '';
+
+    return {
+      interview: {
+        id: entry.interviewId,
+        sessionID: entry.sessionID,
+        idea: entry.idea,
+        markdownPath: displayPath,
+        createdAt: new Date(entry.lastUpdatedAt).toISOString(),
+        status:
+          entry.mode === 'session-disconnected'
+            ? ('abandoned' as const)
+            : ('active' as const),
+        baseMessageCount: 0,
+      },
+      url: `${baseUrl}/interview/${entry.interviewId}`,
+      markdownPath,
+      mode: entry.mode,
+      isBusy: entry.mode === 'awaiting-agent',
+      summary: entry.summary,
+      questions: entry.questions,
+      document,
+      lastUpdatedAt: entry.lastUpdatedAt,
+      nudgeAction: entry.nudgeAction,
+      blocks: entry.blocks ?? parseSpecBlocks(document),
+    };
+  }
+
+  function broadcastSse(interviewId: string, entry: InterviewStateEntry) {
     const clients = sseClients.get(interviewId);
     if (!clients || clients.size === 0) return;
-    const payload = `event: state\ndata: ${JSON.stringify(data)}\n\n`;
+    const payload = `event: state\ndata: ${JSON.stringify(formatSseState(entry))}\n\n`;
     for (const res of clients) {
       try {
         res.write(payload);
@@ -741,7 +774,9 @@ export function createDashboardServer(config: DashboardConfig): {
       clients.add(response);
 
       // Send initial state immediately
-      response.write(`event: state\ndata: ${JSON.stringify(entry)}\n\n`);
+      response.write(
+        `event: state\ndata: ${JSON.stringify(formatSseState(entry))}\n\n`,
+      );
 
       // Heartbeat every 15s to keep connection alive
       const heartbeat = setInterval(() => {
@@ -792,11 +827,14 @@ export function createDashboardServer(config: DashboardConfig): {
         if (state.title) existing.title = state.title;
         if (state.questions) existing.questions = state.questions;
         if (state.filePath) existing.filePath = state.filePath;
+        if (state.document !== undefined) existing.document = state.document;
+        if (state.blocks !== undefined) existing.blocks = state.blocks;
         existing.lastUpdatedAt = Date.now();
         dedupRecovered(interviewId, stateCache);
+        broadcastSse(interviewId, existing);
       } else {
         // New entry
-        stateCache.set(interviewId, {
+        const entry: InterviewStateEntry = {
           interviewId,
           sessionID: state.sessionID ?? '',
           idea: state.idea ?? '',
@@ -810,7 +848,11 @@ export function createDashboardServer(config: DashboardConfig): {
           nudgeAction: null,
           pendingBlockComment: state.pendingBlockComment ?? null,
           pendingChatMessage: state.pendingChatMessage ?? null,
-        });
+          document: state.document,
+          blocks: state.blocks,
+        };
+        stateCache.set(interviewId, entry);
+        broadcastSse(interviewId, entry);
       }
 
       sendJson(response, 200, { status: 'ok' });
@@ -1319,6 +1361,12 @@ export function createDashboardServer(config: DashboardConfig): {
           entry.pendingBlockComment ??= existing.pendingBlockComment;
         if (existing.pendingChatMessage)
           entry.pendingChatMessage ??= existing.pendingChatMessage;
+        if (entry.document === undefined && existing.document !== undefined) {
+          entry.document = existing.document;
+        }
+        if (entry.blocks === undefined && existing.blocks !== undefined) {
+          entry.blocks = existing.blocks;
+        }
       }
       stateCache.set(entry.interviewId, entry);
       dedupRecovered(entry.interviewId, stateCache);

+ 4 - 3
src/interview/document.ts

@@ -119,9 +119,10 @@ export function extractSummarySection(document: string): string {
   const summaryStart = start + marker.length;
   const historyMarker = /\n\n## Q&A history/i;
   const historyMatch = document.slice(summaryStart).match(historyMarker);
-  const summaryEnd = historyMatch?.index
-    ? summaryStart + historyMatch.index
-    : undefined;
+  const summaryEnd =
+    historyMatch?.index !== undefined
+      ? summaryStart + historyMatch.index
+      : undefined;
   return document.slice(summaryStart, summaryEnd).trim();
 }
 

+ 2 - 0
src/interview/manager.ts

@@ -509,6 +509,8 @@ function stateToEntry(
     nudgeAction: null,
     pendingBlockComment: null,
     pendingChatMessage: null,
+    document: state.document,
+    blocks: state.blocks,
   };
 }
 

+ 15 - 18
src/interview/service.ts

@@ -161,6 +161,7 @@ export function createInterviewService(
   const browserOpener = deps?.openBrowser ?? openBrowser;
   const activeInterviewIds = new Map<string, string>();
   const interviewsById = new Map<string, InterviewRecord>();
+  const activeSyncs = new Map<string, Promise<InterviewState>>();
   const sessionBusy = new Map<string, boolean>();
   const sessionModel = new Map<string, string>();
   const browserOpened = new Set<string>(); // Track interviews that have opened browser
@@ -364,7 +365,20 @@ export function createInterviewService(
     return record;
   }
 
-  async function syncInterview(
+  function syncInterview(interview: InterviewRecord): Promise<InterviewState> {
+    const existing = activeSyncs.get(interview.id);
+    if (existing) {
+      return existing;
+    }
+
+    const sync = performSyncInterview(interview).finally(() => {
+      activeSyncs.delete(interview.id);
+    });
+    activeSyncs.set(interview.id, sync);
+    return sync;
+  }
+
+  async function performSyncInterview(
     interview: InterviewRecord,
   ): Promise<InterviewState> {
     const allMessages = await loadMessagesWithRetry(interview.sessionID);
@@ -636,24 +650,7 @@ export function createInterviewService(
       const sessionID = properties.sessionID as string | undefined;
       const status = properties.status as { type?: string } | undefined;
       if (sessionID) {
-        const wasBusy = sessionBusy.get(sessionID) === true;
         sessionBusy.set(sessionID, status?.type === 'busy');
-
-        // Sync state on busy → idle so the browser gets the final push
-        if (wasBusy && status?.type !== 'busy') {
-          const activeId = activeInterviewIds.get(sessionID);
-          const interview = activeId ? interviewsById.get(activeId) : null;
-          if (interview && interview.status === 'active') {
-            syncInterview(interview).catch((err) => {
-              log(
-                '[interview] failed to sync interview state in event handler:',
-                {
-                  error: err instanceof Error ? err.message : String(err),
-                },
-              );
-            });
-          }
-        }
       }
       return;
     }

+ 2 - 0
src/interview/types.ts

@@ -133,4 +133,6 @@ export interface InterviewStateEntry {
     comment: string;
   } | null;
   pendingChatMessage: string | null;
+  document?: string;
+  blocks?: SpecBlock[];
 }