From 1d2ce85cfdf7c5843a421e2e91076a0b0be11b10 Mon Sep 17 00:00:00 2001 From: corpi Date: Mon, 16 Mar 2026 20:08:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(space):=20=EC=A2=85=EB=A3=8C=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EB=AA=A8=EB=8B=AC=EA=B3=BC=20current=20session=20t?= =?UTF-8?q?hought=20=EB=B3=B5=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/90_current_state.md | 9 ++ docs/session_brief.md | 16 +- docs/work.md | 33 ++++- src/entities/session/model/useThoughtInbox.ts | 60 ++++---- .../focus-session/api/focusSessionApi.ts | 63 +++++++- .../model/useFocusSessionEngine.ts | 9 +- src/shared/i18n/messages/space.ts | 11 ++ .../ui/CompletionResultModal.tsx | 138 ++++++++++++++++++ .../model/useSpaceWorkspaceSessionControls.ts | 36 ++--- .../ui/SpaceWorkspaceWidget.tsx | 105 +++++++++++-- 10 files changed, 401 insertions(+), 79 deletions(-) create mode 100644 src/widgets/space-focus-hud/ui/CompletionResultModal.tsx diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 40bbfb2..e1135a7 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -77,6 +77,15 @@ Last Updated: 2026-03-16 - timer가 `00:00`에 도달하면 same-session modal이 자동으로 열리고, `완료하고 종료하기 / 10분 더` 두 경로만 제안한다 - `10분 더`를 누르면 현재 focus phase에 10분이 추가되어 바로 running으로 돌아가고, 다시 시간이 끝나면 같은 modal이 다시 열린다 - server와 web 모두 `extend-phase` 계약 기준으로 동작한다 + - `10분 더`는 남은 시간만이 아니라 `focusDurationSeconds`도 함께 늘려 이후 결과 모달과 review의 집중 시간이 맞도록 정리됐다 +- `/space` completion result modal 추가: + - `timer-complete -> 완료하고 종료하기`와 `End Session -> 여기서 마무리하기`는 즉시 `/app`으로 가지 않고 중앙 결과 모달을 먼저 연다 + - 결과 모달에는 `집중한 시간`, `완료한 목표`, `이번 세션 thought capsule`이 표시된다 + - 결과 모달이 떠 있는 동안 `/space` 자동 `/app` redirect는 막히고, `확인하고 돌아가기`에서만 `/app`으로 이동한다 +- current-session thought capsule 서버 복원: + - `/space` thought는 public `focusSessionId` 없이 auth 기반으로 현재 세션에 서버가 내부 연결한다 + - `/space` 진입 시 `GET /api/v1/focus-sessions/current/thoughts`로 현재 세션 thought 목록을 복원한다 + - 브라우저를 껐다 켜도 current session이 살아 있으면 같은 thought 목록을 다시 읽을 수 있다 - `/space` intent HUD collapsed / expanded 재설계: - 상시 큰 goal 카드 대신 idle에서는 goal 1줄만 남는 collapsed glass rail 구조로 변경 - hover / focus / rail tap에서만 expanded card로 열리며, 이때만 microStep과 `이번 목표 완료` 액션이 노출됨 diff --git a/docs/session_brief.md b/docs/session_brief.md index 7394c1a..765d6d6 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -14,10 +14,10 @@ Last Updated: 2026-03-16 ## 현재 우선순위 -1. `/space` current-session-only cleanup -2. `Core Loop Alignment` browser audit -3. `Weekly Review` carry-forward 고도화 -4. `Premium Ambience` polish +1. `/space` completion result modal browser QA +2. `/space` current-session-only cleanup +3. `Core Loop Alignment` browser audit +4. `Weekly Review` carry-forward 고도화 ## 최근 세션 상태 @@ -87,6 +87,14 @@ Last Updated: 2026-03-16 - `00:00`이 되면 `완료하고 종료하기 / 10분 더` 모달이 자동으로 열린다. - `10분 더`는 server `extend-phase` 계약을 타고 현재 focus phase를 10분 연장한 뒤 다시 running으로 이어진다. - 그래서 사용자는 recovery 상태에서도 `계속 / 다시 잡기 / 마무리` 중 하나를 바로 고를 수 있다. +- `/space` 종료 결과 모달이 추가됐다. + - `완료하고 종료하기`와 `여기서 마무리하기`는 바로 `/app`으로 가지 않고 중앙 결과 모달을 먼저 띄운다. + - 결과 모달에는 `집중한 시간`, `완료한 목표`, `이번 세션 thought capsule`이 들어간다. + - 닫기 전까지 `/space`는 결과 모달 상태로 유지되고, `확인하고 돌아가기`에서만 `/app`으로 이동한다. +- thought capsule은 서버가 현재 세션에 내부 귀속한다. + - 클라이언트는 `focusSessionId`를 보내지 않는다. + - `/space`는 current session이 살아 있으면 server `current thoughts` API로 same-session thought 목록을 복원한다. + - 그래서 브라우저 재시작 후에도 같은 세션이라면 결과 모달에 같은 thought들이 포함된다. - `/space` 목표 카드를 collapsed / expanded 구조로 재설계했다. - idle에서는 goal 1줄만 남는 얇은 glass rail로 줄였다. - microStep과 `이번 목표 완료`는 expanded 상태에서만 드러난다. diff --git a/docs/work.md b/docs/work.md index f87bf58..de1a570 100644 --- a/docs/work.md +++ b/docs/work.md @@ -18,6 +18,31 @@ ## 작업 1 +- 제목: `/space` completion result modal browser QA +- 목적: + - 세션 완전 종료 직후 결과 모달이 자연스럽게 뜨는지 확인한다. + - timer-complete, End Session finish, 10분 더 이후 재종료, thought 복원 흐름을 실제 브라우저에서 검증한다. +- 변경 범위: + - `/space` timer-complete finish + - `/space` End Session finish + - current-session thoughts restore + - 결과 모달 -> `/app` +- 제외 범위: + - 새로운 stats/weekly review 기능 추가 금지 +- 완료 조건: + - 종료 직후 중앙 결과 모달이 보인다 + - 결과 모달에 집중 시간/목표/thought가 맞게 보인다 + - 결과 모달 확인 후에만 `/app`으로 이동한다 + - 브라우저 재실행 뒤 current session이 살아 있으면 thought capture가 유지된다 +- 진행 상태: + - 다음 작업 +- 검증: + - manual browser QA +- 커밋 힌트: + - docs(qa): completion result modal browser audit 기록 + +## 작업 2 + - 제목: `/app` Atmosphere Entry Shell - 목적: - `docs/screens/app/current/19_app_atmosphere_entry_spec.md` 기준으로 `/app` no-session 상태를 `goal + duration + atmosphere` 중심의 premium entry screen으로 재설계한다. @@ -44,7 +69,7 @@ - 커밋 힌트: - feat(app): atmosphere entry shell 1차 구현 -## 작업 2 +## 작업 3 - 제목: `Custom Duration Contract` - 목적: @@ -61,13 +86,13 @@ - `70분` 같은 값이 실제 focus duration으로 반영된다 - break duration이 정책 기준으로 계산된다 - 진행 상태: - - 다음 작업 + - 구현 완료 - 검증: - start -> `/space` -> timer duration 확인 - 커밋 힌트: - feat(flow): custom duration contract 연결 -## 작업 3 +## 작업 4 - 제목: `Weekly Review Dock Reposition` - 목적: @@ -89,7 +114,7 @@ - 커밋 힌트: - fix(app): review dock 위치 재정렬 -## 작업 4 +## 작업 5 - 제목: `Core Loop Alignment Audit` browser slice - 목적: diff --git a/src/entities/session/model/useThoughtInbox.ts b/src/entities/session/model/useThoughtInbox.ts index 0720bf8..9484690 100644 --- a/src/entities/session/model/useThoughtInbox.ts +++ b/src/entities/session/model/useThoughtInbox.ts @@ -7,6 +7,20 @@ import type { RecentThought } from './types'; const MAX_THOUGHT_INBOX_ITEMS = 40; +const normalizeThought = (thought: { + id: string; + text: string; + sceneName: string; + isCompleted: boolean; + capturedAt: string; +}): RecentThought => ({ + id: thought.id, + text: thought.text, + sceneName: thought.sceneName, + isCompleted: thought.isCompleted, + capturedAt: thought.capturedAt, +}); + export const useThoughtInbox = () => { const [thoughts, setThoughts] = useState([]); @@ -16,15 +30,7 @@ export const useThoughtInbox = () => { inboxApi.getThoughts() .then((data) => { if (!mounted) return; - setThoughts( - data.map((d) => ({ - id: d.id, - text: d.text, - sceneName: d.sceneName, - isCompleted: d.isCompleted, - capturedAt: copy.session.justNow, - })) - ); + setThoughts(data.map(normalizeThought)); }) .catch((err) => { console.error('Failed to load inbox thoughts:', err); @@ -35,7 +41,7 @@ export const useThoughtInbox = () => { }; }, []); - const addThought = useCallback((text: string, sceneName: string) => { + const addThought = useCallback(async (text: string, sceneName: string) => { const trimmedText = text.trim(); if (!trimmedText) { @@ -57,18 +63,20 @@ export const useThoughtInbox = () => { return next; }); - inboxApi.addThought({ text: trimmedText, sceneName }) - .then((res) => { - setThoughts((current) => - current.map((t) => (t.id === tempId ? { ...t, id: res.id } : t)) - ); - }) - .catch((err) => { - console.error('Failed to add thought:', err); - setThoughts((current) => current.filter((t) => t.id !== tempId)); - }); + try { + const response = await inboxApi.addThought({ text: trimmedText, sceneName }); + const normalized = normalizeThought(response); - return thought; + setThoughts((current) => + current.map((t) => (t.id === tempId ? normalized : t)) + ); + + return normalized; + } catch (err) { + console.error('Failed to add thought:', err); + setThoughts((current) => current.filter((t) => t.id !== tempId)); + return null; + } }, []); const removeThought = useCallback((thoughtId: string) => { @@ -140,15 +148,7 @@ export const useThoughtInbox = () => { return inboxApi.getThoughts(); }) .then((data) => { - setThoughts( - data.map((d) => ({ - id: d.id, - text: d.text, - sceneName: d.sceneName, - isCompleted: d.isCompleted, - capturedAt: copy.session.justNow, - })) - ); + setThoughts(data.map(normalizeThought)); }) .catch((err) => { console.error('Failed to restore thoughts:', err); diff --git a/src/features/focus-session/api/focusSessionApi.ts b/src/features/focus-session/api/focusSessionApi.ts index 0a3b351..7906fdf 100644 --- a/src/features/focus-session/api/focusSessionApi.ts +++ b/src/features/focus-session/api/focusSessionApi.ts @@ -31,6 +31,22 @@ interface RawAdvanceCurrentGoalResponse { updatedPlanToday: Parameters[0]; } +interface RawCurrentSessionThought { + id: string; + text: string; + sceneName: string; + isCompleted: boolean; + capturedAt: string; +} + +interface RawCompletionResult { + completedSessionId: string; + completionSource: 'timer-complete' | 'manual-end'; + completedGoal: string; + focusedSeconds: number; + thoughts: RawCurrentSessionThought[]; +} + export interface FocusSession { id: string; sceneId: string; @@ -52,6 +68,22 @@ export interface FocusSession { serverNow: string; } +export interface CurrentSessionThought { + id: string; + text: string; + sceneName: string; + isCompleted: boolean; + capturedAt: string; +} + +export interface CompletionResult { + completedSessionId: string; + completionSource: 'timer-complete' | 'manual-end'; + completedGoal: string; + focusedSeconds: number; + thoughts: CurrentSessionThought[]; +} + export interface StartFocusSessionRequest { sceneId: string; goal: string; @@ -126,6 +158,23 @@ const normalizeAdvanceGoalResponse = ( }; }; +const normalizeCurrentSessionThought = ( + thought: RawCurrentSessionThought, +): CurrentSessionThought => { + return { + ...thought, + }; +}; + +const normalizeCompletionResult = ( + result: RawCompletionResult, +): CompletionResult => { + return { + ...result, + thoughts: result.thoughts.map(normalizeCurrentSessionThought), + }; +}; + export const focusSessionApi = { getCurrentSession: async (): Promise => { const response = await apiClient('api/v1/focus-sessions/current', { @@ -135,6 +184,14 @@ export const focusSessionApi = { return response ? normalizeFocusSession(response) : null; }, + getCurrentSessionThoughts: async (): Promise => { + const response = await apiClient('api/v1/focus-sessions/current/thoughts', { + method: 'GET', + }); + + return response.map(normalizeCurrentSessionThought); + }, + startSession: async (payload: StartFocusSessionRequest): Promise => { const response = await apiClient('api/v1/focus-sessions', { method: 'POST', @@ -202,13 +259,13 @@ export const focusSessionApi = { return normalizeFocusSession(response); }, - completeSession: async (payload: CompleteFocusSessionRequest): Promise => { - const response = await apiClient('api/v1/focus-sessions/current/complete', { + completeSession: async (payload: CompleteFocusSessionRequest): Promise => { + const response = await apiClient('api/v1/focus-sessions/current/complete', { method: 'POST', body: JSON.stringify(payload), }); - return normalizeFocusSession(response); + return normalizeCompletionResult(response); }, advanceGoal: async (payload: AdvanceCurrentGoalRequest): Promise => { diff --git a/src/features/focus-session/model/useFocusSessionEngine.ts b/src/features/focus-session/model/useFocusSessionEngine.ts index 6d0dea7..5c05cba 100644 --- a/src/features/focus-session/model/useFocusSessionEngine.ts +++ b/src/features/focus-session/model/useFocusSessionEngine.ts @@ -5,6 +5,7 @@ import { copy } from '@/shared/i18n'; import { type AdvanceCurrentGoalRequest, type AdvanceCurrentGoalResponse, + type CompletionResult, focusSessionApi, type CompleteFocusSessionRequest, type FocusSession, @@ -76,7 +77,7 @@ interface UseFocusSessionEngineResult { extendCurrentPhase: (payload: { additionalMinutes: number }) => Promise; updateCurrentIntent: (payload: UpdateCurrentFocusSessionIntentRequest) => Promise; updateCurrentSelection: (payload: UpdateCurrentFocusSessionSelectionRequest) => Promise; - completeSession: (payload: CompleteFocusSessionRequest) => Promise; + completeSession: (payload: CompleteFocusSessionRequest) => Promise; advanceGoal: (payload: AdvanceCurrentGoalRequest) => Promise; abandonSession: () => Promise; clearError: () => void; @@ -279,16 +280,16 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => { return null; } - const session = await runMutation( + const result = await runMutation( () => focusSessionApi.completeSession(payload), copy.focusSession.completeFailed, ); - if (session) { + if (result) { applySession(null); } - return session; + return result; }, advanceGoal: async (payload) => { if (!currentSession) { diff --git a/src/shared/i18n/messages/space.ts b/src/shared/i18n/messages/space.ts index a389b91..3e47b2f 100644 --- a/src/shared/i18n/messages/space.ts +++ b/src/shared/i18n/messages/space.ts @@ -122,6 +122,17 @@ export const space = { confirmPending: '시작 중…', finishPending: '마무리 중…', }, + completionResult: { + eyebrow: 'SESSION COMPLETE', + title: '이번 세션을 조용히 닫아둘게요.', + description: '얼마나 집중했는지, 무엇을 끝냈는지, 생각 캡슐에 남긴 것까지 한 번에 정리해둡니다.', + focusedLabel: '집중한 시간', + focusedValue: (minutes: number) => `${minutes}분`, + goalLabel: '완료한 목표', + thoughtsLabel: '이번 세션 생각 캡슐', + thoughtCount: (count: number) => `${count}개`, + confirmButton: '확인하고 돌아가기', + }, controlCenter: { sectionTitles: { background: 'Background', diff --git a/src/widgets/space-focus-hud/ui/CompletionResultModal.tsx b/src/widgets/space-focus-hud/ui/CompletionResultModal.tsx new file mode 100644 index 0000000..f48ab30 --- /dev/null +++ b/src/widgets/space-focus-hud/ui/CompletionResultModal.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { copy } from '@/shared/i18n'; +import { cn } from '@/shared/lib/cn'; +import type { CompletionResult } from '@/features/focus-session'; + +interface CompletionResultModalProps { + open: boolean; + result: CompletionResult | null; + onClose: () => void; +} + +const formatFocusedMinutes = (focusedSeconds: number) => { + const safeSeconds = Math.max(0, focusedSeconds); + return Math.round(safeSeconds / 60); +}; + +export const CompletionResultModal = ({ + open, + result, + onClose, +}: CompletionResultModalProps) => { + if (!result) { + return null; + } + + const focusedMinutes = formatFocusedMinutes(result.focusedSeconds); + const hasThoughts = result.thoughts.length > 0; + + return ( +
+
+ +
+
+
+
+
+ +
+

+ {copy.space.completionResult.eyebrow} +

+

+ {copy.space.completionResult.title} +

+

+ {copy.space.completionResult.description} +

+
+ +
+
+

+ {copy.space.completionResult.focusedLabel} +

+

+ {copy.space.completionResult.focusedValue(focusedMinutes)} +

+
+ +
+

+ {copy.space.completionResult.goalLabel} +

+

+ {result.completedGoal} +

+
+ + {hasThoughts ? ( +
+
+

+ {copy.space.completionResult.thoughtsLabel} +

+

+ {copy.space.completionResult.thoughtCount(result.thoughts.length)} +

+
+
+ {result.thoughts.map((thought) => ( +
+

+ {thought.text} +

+

+ {thought.sceneName} +

+
+ ))} +
+
+ ) : null} +
+ +
+ +
+
+
+
+ ); +}; diff --git a/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts b/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts index 658cce7..d5b0b52 100644 --- a/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts +++ b/src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useEffect, useRef } from 'react'; -import type { FocusSession } from '@/features/focus-session'; +import type { CompletionResult, FocusSession } from '@/features/focus-session'; import { copy } from '@/shared/i18n'; import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine'; import { findAtmosphereOptionForSelection } from '@/widgets/focus-dashboard/model/atmosphereEntry'; @@ -47,7 +47,7 @@ interface UseSpaceWorkspaceSessionControlsParams { completedGoal?: string; focusScore?: number; distractionCount?: number; - }) => Promise; + }) => Promise; advanceGoal: (input: { completedGoal: string; nextGoal: string; @@ -313,39 +313,33 @@ export const useSpaceWorkspaceSessionControls = ({ const trimmedCurrentGoal = goalInput.trim(); if (!currentSession) { - return false; + return null; } - const completedSession = await completeSession({ + const completionResult = await completeSession({ completionType: 'goal-complete', completedGoal: trimmedCurrentGoal || undefined, }); - if (!completedSession) { + if (!completionResult) { pushStatusLine({ message: copy.space.workspace.goalCompleteSyncFailed, }); - return false; + return null; } - setGoalInput(''); - setLinkedFocusPlanItemId(null); - setSelectedGoalId(null); setShowResumePrompt(false); setPendingSessionEntryPoint('space-setup'); setPreviewPlaybackState('paused'); setWorkspaceMode('setup'); - return true; + return completionResult; }, [ completeSession, currentSession, goalInput, pushStatusLine, - setGoalInput, - setLinkedFocusPlanItemId, setPendingSessionEntryPoint, setPreviewPlaybackState, - setSelectedGoalId, setShowResumePrompt, setWorkspaceMode, ]); @@ -354,39 +348,33 @@ export const useSpaceWorkspaceSessionControls = ({ const trimmedCurrentGoal = goalInput.trim(); if (!currentSession) { - return false; + return null; } - const completedSession = await completeSession({ + const completionResult = await completeSession({ completionType: 'timer-complete', completedGoal: trimmedCurrentGoal || undefined, }); - if (!completedSession) { + if (!completionResult) { pushStatusLine({ message: copy.space.workspace.timerCompleteSyncFailed, }); - return false; + return null; } - setGoalInput(''); - setLinkedFocusPlanItemId(null); - setSelectedGoalId(null); setShowResumePrompt(false); setPendingSessionEntryPoint('space-setup'); setPreviewPlaybackState('paused'); setWorkspaceMode('setup'); - return true; + return completionResult; }, [ completeSession, currentSession, goalInput, pushStatusLine, - setGoalInput, - setLinkedFocusPlanItemId, setPendingSessionEntryPoint, setPreviewPlaybackState, - setSelectedGoalId, setShowResumePrompt, setWorkspaceMode, ]); diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index f9b67d6..31622d6 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -9,7 +9,12 @@ import { import { usePlanTier } from "@/entities/plan"; import { getSceneById, SCENE_THEMES } from "@/entities/scene"; import { GOAL_CHIPS, SOUND_PRESETS, useThoughtInbox } from "@/entities/session"; -import { useFocusSessionEngine } from "@/features/focus-session"; +import { + focusSessionApi, + type CompletionResult, + type CurrentSessionThought, + useFocusSessionEngine, +} from "@/features/focus-session"; import { useFocusStats } from "@/features/stats"; import { useSoundPlayback, @@ -19,6 +24,7 @@ import { useHudStatusLine } from "@/shared/lib/useHudStatusLine"; import { copy } from "@/shared/i18n"; import { SpaceFocusHudWidget } from "@/widgets/space-focus-hud"; import { SpaceSetupDrawerWidget } from "@/widgets/space-setup-drawer"; +import { CompletionResultModal } from "@/widgets/space-focus-hud/ui/CompletionResultModal"; import { findAtmosphereOptionForSelection, getRecommendedDurationMinutes, @@ -83,6 +89,8 @@ export const SpaceWorkspaceWidget = () => { const [pendingSessionEntryPoint, setPendingSessionEntryPoint] = useState("space-setup"); const [showReviewTeaserAfterComplete, setShowReviewTeaserAfterComplete] = useState(false); + const [, setCurrentSessionThoughts] = useState([]); + const [pendingCompletionResult, setPendingCompletionResult] = useState(null); const { selectedPresetId, @@ -107,10 +115,12 @@ export const SpaceWorkspaceWidget = () => { advanceGoal, abandonSession, } = useFocusSessionEngine(); + const isCompletionResultOpen = pendingCompletionResult !== null; const isFocusMode = workspaceMode === "focus"; const resolvedPlaybackState = currentSession?.state ?? previewPlaybackState; const shouldPlaySound = isFocusMode && resolvedPlaybackState === "running"; + const currentSessionId = currentSession?.id ?? null; const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode); @@ -207,10 +217,10 @@ export const SpaceWorkspaceWidget = () => { : undefined; useEffect(() => { - if (!isBootstrapping && !currentSession) { + if (!isBootstrapping && !currentSession && !pendingCompletionResult) { router.replace("/app"); } - }, [isBootstrapping, currentSession, router]); + }, [isBootstrapping, currentSession, pendingCompletionResult, router]); useEffect(() => { if (isBootstrapping || didResolveEntryRouteRef.current) { @@ -224,6 +234,41 @@ export const SpaceWorkspaceWidget = () => { } }, [currentSession, isBootstrapping]); + useEffect(() => { + let mounted = true; + + const syncCurrentSessionThoughts = async () => { + if (!currentSessionId) { + if (!pendingCompletionResult && mounted) { + setCurrentSessionThoughts([]); + } + return; + } + + try { + const thoughts = await focusSessionApi.getCurrentSessionThoughts(); + + if (!mounted) { + return; + } + + setCurrentSessionThoughts(thoughts); + } catch { + if (!mounted) { + return; + } + + setCurrentSessionThoughts([]); + } + }; + + void syncCurrentSessionThoughts(); + + return () => { + mounted = false; + }; + }, [currentSessionId, pendingCompletionResult]); + useEffect(() => { const preferMobile = typeof window !== "undefined" @@ -258,7 +303,7 @@ export const SpaceWorkspaceWidget = () => {
{ sessionPhase={phase ?? 'focus'} onIntentUpdate={controls.handleIntentUpdate} onGoalFinish={async () => { - const didFinish = await controls.handleGoalComplete(); + const completionResult = await controls.handleGoalComplete(); - if (didFinish) { - setShowReviewTeaserAfterComplete(true); + if (completionResult) { + setPendingCompletionResult(completionResult); + setCurrentSessionThoughts([]); } - return didFinish; + return Boolean(completionResult); + }} + onTimerFinish={async () => { + const completionResult = await controls.handleTimerComplete(); + + if (completionResult) { + setPendingCompletionResult(completionResult); + setCurrentSessionThoughts([]); + } + + return Boolean(completionResult); }} - onTimerFinish={controls.handleTimerComplete} onAddTenMinutes={() => controls.handleExtendCurrentPhase(10)} onGoalUpdate={controls.handleGoalAdvance} onStatusMessage={pushStatusLine} - onCaptureThought={(note) => addThought(note, selection.selectedScene.name)} + onCaptureThought={(note) => { + void addThought(note, selection.selectedScene.name).then((savedThought) => { + if (!savedThought) { + return; + } + + setCurrentSessionThoughts((current) => { + if (current.some((thought) => thought.id === savedThought.id)) { + return current; + } + + return [...current, { + id: savedThought.id, + text: savedThought.text, + sceneName: savedThought.sceneName, + capturedAt: savedThought.capturedAt, + isCompleted: savedThought.isCompleted ?? false, + }]; + }); + }); + }} onExitRequested={() => void controls.handleExitRequested()} /> ) : null} + { + setPendingCompletionResult(null); + setCurrentSessionThoughts([]); + router.replace('/app'); + }} + /> +