loads of polish and bug fixes

This commit is contained in:
Chris Sanden
2026-05-05 15:41:44 +02:00
parent a4f99a50d0
commit 2bb2ac63a0
8 changed files with 379 additions and 53 deletions

View File

@@ -1,6 +1,5 @@
import {
GetActiveSession,
RemoveActiveSession,
type ActiveSession,
} from '@/lib/asyncStorage';
import type { SessionType } from '@/lib/types';
@@ -8,6 +7,7 @@ import { formatDate, formatDateTime } from '@/lib/date';
import { RegisterForLocalNotificationsAsync } from '@/lib/notifications';
import { CheckAssignmentCompletion } from '@/lib/progress';
import { DEFAULT_FOCUS_DURATION_MINUTES } from '@/lib/sessionDefaults';
import { finalizeStoredSession } from '@/lib/sessionLifecycle';
import { supabase } from "@/lib/supabase";
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { Session } from '@supabase/supabase-js';
@@ -182,7 +182,7 @@ export default function HomeScreen() {
);
if (secondsLeft <= 0) {
await RemoveActiveSession();
await finalizeStoredSession('expired', storedSprint);
setActiveSprint(null);
setActiveSprintTaskTitle(null);
setActiveSprintTaskDesc(null);
@@ -501,7 +501,7 @@ export default function HomeScreen() {
setRemainingSeconds(secondsLeft);
if (secondsLeft <= 0) {
void RemoveActiveSession();
void finalizeStoredSession('expired', activeSprint);
setActiveSprint(null);
setActiveSprintTaskTitle(null);
setActiveSprintTaskDesc(null);
@@ -563,7 +563,7 @@ export default function HomeScreen() {
const secondsLeft = Math.ceil((storedSession.endTime - Date.now()) / 1000);
if (secondsLeft <= 0) {
await RemoveActiveSession();
await finalizeStoredSession('expired', storedSession);
router.push({
pathname: '/task/timer',
params: {
@@ -597,7 +597,7 @@ export default function HomeScreen() {
text: 'Start sprint',
style: 'destructive',
onPress: async () => {
await RemoveActiveSession();
await finalizeStoredSession('cancelled', storedSession);
router.push({
pathname: '/task/timer',
params: {

View File

@@ -1,4 +1,5 @@
import { GetActiveSession, RemoveActiveSession, type ActiveSession } from '@/lib/asyncStorage';
import { GetActiveSession, type ActiveSession } from '@/lib/asyncStorage';
import { finalizeStoredSession } from '@/lib/sessionLifecycle';
import { supabase } from '@/lib/supabase';
import { Session } from '@supabase/supabase-js';
import { Redirect, Stack, router, useFocusEffect, useLocalSearchParams } from 'expo-router';
@@ -121,7 +122,7 @@ export default function SetupScreen() {
]);
if (storedActiveSession && storedActiveSession.endTime <= Date.now()) {
await RemoveActiveSession();
await finalizeStoredSession('expired', storedActiveSession);
setActiveSession(null);
} else {
setActiveSession(storedActiveSession);
@@ -218,7 +219,7 @@ export default function SetupScreen() {
}
if (freshActiveSession) {
await RemoveActiveSession();
await finalizeStoredSession('expired', freshActiveSession);
setActiveSession(null);
}

View File

@@ -1,13 +1,20 @@
import {
GetActiveSession,
RemoveActiveSession,
GetStudyCycle,
RemoveStudyCycle,
SaveActiveSession,
SaveStudyCycle,
type ActiveSession,
type StudyCycle,
} from '@/lib/asyncStorage';
import {
DEFAULT_FOCUS_DURATION_MINUTES,
DEFAULT_LONG_BREAK_DURATION_MINUTES,
DEFAULT_SHORT_BREAK_DURATION_MINUTES,
FOCUS_SESSIONS_PER_LONG_BREAK,
STUDY_CYCLE_IDLE_RESET_MINUTES,
} from '@/lib/sessionDefaults';
import { finalizeStoredSession } from '@/lib/sessionLifecycle';
import { supabase } from '@/lib/supabase';
import type { SessionType, Task } from '@/lib/types';
import * as Haptics from 'expo-haptics';
@@ -33,17 +40,6 @@ const colors = {
text: '#ffffff',
};
/*
TODO
Make timer count down even when app is un-focused or closed.
Set const endTime = Date.now() + duration and save that to the task, maybe?
Then trigger notif when endTime == Date.now()?
Then fetch endTime from DB -> if null then timer is inactive
if !null then set timer to endTime - Date.now() and start
Might have to save duration as well in DB to preserve timer animation persistance
*/
const TIMER_OPTIONS = [...Array(13).keys()].map((index) => (index === 0 ? 1 : index * 5));
const ITEM_SIZE = width * 0.38;
const ITEM_SPACING = (width - ITEM_SIZE) / 2;
@@ -52,11 +48,24 @@ const HOLD_TO_CANCEL_MS = 2000;
const CANCEL_ANIMATION_DELAY_MS = 250;
const BUTTON_PRESS_IN_MS = 80;
const BUTTON_PRESS_OUT_MS = 140;
const STUDY_CYCLE_IDLE_RESET_MS = STUDY_CYCLE_IDLE_RESET_MINUTES * 60 * 1000;
type PostSessionPrompt = {
completedSessionType: SessionType;
returnTaskId: string | null;
nextBreakType: 'short_break' | 'long_break' | null;
};
type BreakSessionType = Extract<SessionType, 'short_break' | 'long_break'>;
function isBreakSession(sessionType: SessionType): sessionType is BreakSessionType {
return sessionType === 'short_break' || sessionType === 'long_break';
}
function isStudyCycleActive(studyCycle: StudyCycle, taskId: string, now: number) {
return studyCycle.taskId === taskId && now - studyCycle.lastCompletedAt <= STUDY_CYCLE_IDLE_RESET_MS;
}
function formatTime(totalSeconds: number) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
@@ -91,6 +100,7 @@ function getSessionLabel(sessionType: SessionType) {
type StartSessionInput = {
sessionType: SessionType;
taskId: string | null;
returnTaskId: string | null;
durationSeconds: number;
};
@@ -208,7 +218,7 @@ export default function TimerScreen() {
animated: false,
});
});
}, [pickerDuration, scrollX, showDurationPicker]);
}, [pickerDuration, scrollX, showDurationPicker, timerIsRunning]);
React.useEffect(() => {
if (containerHeight > 0 && !timerIsRunning) {
@@ -294,24 +304,12 @@ export default function TimerScreen() {
finalStatus: 'completed' | 'cancelled' | 'expired',
activeSessionOverride?: ActiveSession | null
) => {
const activeSession = activeSessionOverride ?? await GetActiveSession();
const result = await finalizeStoredSession(finalStatus, activeSessionOverride);
if (!activeSession) {
return;
}
await RemoveActiveSession();
const { error } = await supabase.rpc('finalize_sprint_session', {
p_session_id: activeSession.sessionId,
p_final_status: finalStatus,
p_ended_at: new Date().toISOString(),
});
if (error) {
if (result?.error) {
Alert.alert(
'Could not finalize sprint session',
error.message
result.error.message
);
}
}, []);
@@ -376,9 +374,69 @@ export default function TimerScreen() {
setTimerOverlayVisible(false);
setTimeRemaining(0);
setCurrentSessionType(selectedSessionType);
setPostSessionPrompt(null);
setIsRunning(false);
}, [cancelOverlayAnimation, selectedSessionType, timerAnimation, timerOverlayOffscreenY]);
const syncStudyCycleAfterCompletion = React.useCallback(
async (
completedSessionType: SessionType,
completedTaskId: string | null
): Promise<'short_break' | 'long_break' | null> => {
const now = Date.now();
if (completedSessionType === 'focus') {
if (!completedTaskId) {
await RemoveStudyCycle();
return 'short_break';
}
const currentStudyCycle = await GetStudyCycle();
const nextCompletedFocusSessions =
currentStudyCycle && isStudyCycleActive(currentStudyCycle, completedTaskId, now)
? currentStudyCycle.completedFocusSessions + 1
: 1;
await SaveStudyCycle({
taskId: completedTaskId,
completedFocusSessions: nextCompletedFocusSessions,
lastCompletedSessionType: 'focus',
lastCompletedAt: now,
});
return nextCompletedFocusSessions % FOCUS_SESSIONS_PER_LONG_BREAK === 0
? 'long_break'
: 'short_break';
}
if (!isBreakSession(completedSessionType) || !completedTaskId) {
await RemoveStudyCycle();
return null;
}
const currentStudyCycle = await GetStudyCycle();
if (!currentStudyCycle || !isStudyCycleActive(currentStudyCycle, completedTaskId, now)) {
await RemoveStudyCycle();
return null;
}
if (completedSessionType === 'long_break') {
await RemoveStudyCycle();
return null;
}
await SaveStudyCycle({
...currentStudyCycle,
lastCompletedSessionType: completedSessionType,
lastCompletedAt: now,
});
return null;
},
[]
);
const finishTimer = React.useCallback(() => {
clearCountdownInterval();
const activeSessionPromise = GetActiveSession();
@@ -418,13 +476,18 @@ export default function TimerScreen() {
const completedReturnTaskId =
completedSessionType === 'focus'
? (completedSession?.taskId ?? tId ?? null)
: (returnTaskId ?? null);
: (completedSession?.returnTaskId ?? returnTaskId ?? null);
const nextBreakType = await syncStudyCycleAfterCompletion(
completedSessionType,
completedReturnTaskId
);
setIsRunning(false);
resetSessionValues();
setPostSessionPrompt({
completedSessionType,
returnTaskId: completedReturnTaskId,
nextBreakType,
});
await finalizeSprintSession('completed', completedSession);
@@ -440,6 +503,7 @@ export default function TimerScreen() {
finalizeSprintSession,
focusModeAnimation,
resetSessionValues,
syncStudyCycleAfterCompletion,
taskDetailsAnimation,
returnTaskId,
tId,
@@ -603,6 +667,7 @@ export default function TimerScreen() {
const startSession = React.useCallback(async ({
sessionType,
taskId,
returnTaskId,
durationSeconds,
}: StartSessionInput) => {
if (timerIsRunning || containerHeight === 0) {
@@ -614,6 +679,15 @@ export default function TimerScreen() {
return;
}
if (sessionType === 'focus' && taskId) {
const currentStudyCycle = await GetStudyCycle();
const now = Date.now();
if (currentStudyCycle && !isStudyCycleActive(currentStudyCycle, taskId, now)) {
await RemoveStudyCycle();
}
}
const endTime = Date.now() + durationSeconds * 1000;
const { data: userData, error: userError } = await supabase.auth.getUser();
@@ -656,6 +730,7 @@ export default function TimerScreen() {
sessionId,
sessionType,
taskId,
returnTaskId,
durationSeconds,
endTime,
});
@@ -684,6 +759,7 @@ export default function TimerScreen() {
await startSession({
sessionType: selectedSessionType,
taskId: selectedSessionType === 'focus' ? (tId ?? null) : null,
returnTaskId: selectedSessionType === 'focus' ? null : (returnTaskId ?? null),
durationSeconds: totalSeconds,
});
}, [
@@ -694,20 +770,27 @@ export default function TimerScreen() {
selectedSessionType,
showDurationPicker,
startSession,
returnTaskId,
tId,
]);
const handleStartShortBreak = React.useCallback(() => {
const handleStartBreak = React.useCallback(() => {
const nextBreakType = postSessionPrompt?.nextBreakType ?? 'short_break';
const durationMinutes =
nextBreakType === 'long_break'
? DEFAULT_LONG_BREAK_DURATION_MINUTES
: DEFAULT_SHORT_BREAK_DURATION_MINUTES;
setPostSessionPrompt(null);
router.replace({
pathname: '/task/timer',
params: {
sessionType: 'short_break',
durationMinutes: String(DEFAULT_SHORT_BREAK_DURATION_MINUTES),
returnTaskId: tId ?? undefined,
sessionType: nextBreakType,
durationMinutes: String(durationMinutes),
returnTaskId: postSessionPrompt?.returnTaskId ?? tId ?? undefined,
},
});
}, [tId]);
}, [postSessionPrompt, tId]);
const handleContinueSameTask = React.useCallback(() => {
if (!postSessionPrompt?.returnTaskId) {
@@ -1092,8 +1175,8 @@ export default function TimerScreen() {
</Text>
<Text style={styles.fixedDurationDescription}>
{selectedSessionType === 'focus'
? 'This sprint uses the default focus duration so you can begin immediately.'
: 'This session uses a fixed duration so you can move straight into the next step.'}
? 'This sprint uses a focused default duration so it is easy to sit down, begin, and keep your study session structured.'
: 'This break has a fixed duration so rest stays intentional and your study rhythm does not get lost.'}
</Text>
{selectedSessionType === 'focus' ? (
<TouchableOpacity onPress={handleChooseCustomDuration} style={styles.durationPickerLink}>
@@ -1118,8 +1201,8 @@ export default function TimerScreen() {
</Text>
<Text style={styles.taskDescription}>
{currentSessionType === 'focus'
? task?.description || 'Focus on this task until the timer ends.'
: 'Use this timer as a real break before starting the next focus session.'}
? task?.description || 'Give this task your full attention for one clear stretch so studying feels deliberate, not scattered.'
: 'Take a proper pause here so the next focus session starts with better energy and structure.'}
</Text>
</Animated.View>
@@ -1134,14 +1217,18 @@ export default function TimerScreen() {
</Text>
<Text style={styles.postSessionBody}>
{postSessionPrompt.completedSessionType === 'focus'
? 'Take a short break, jump straight into another sprint on the same task, or head back to the dashboard.'
: 'Jump back into the same task or head back to the dashboard.'}
? `Take a ${postSessionPrompt.nextBreakType === 'long_break' ? 'long' : 'short'} break to keep your study session structured, jump straight into another sprint on the same task, or head back to the dashboard.`
: 'Continue with the same task when you are ready, or head back to the dashboard if you want to pause the study flow here.'}
</Text>
{postSessionPrompt.completedSessionType === 'focus' ? (
<>
<TouchableOpacity onPress={handleStartShortBreak} style={styles.postSessionPrimaryButton}>
<Text style={styles.postSessionPrimaryButtonText}>Start short break</Text>
<TouchableOpacity onPress={handleStartBreak} style={styles.postSessionPrimaryButton}>
<Text style={styles.postSessionPrimaryButtonText}>
{postSessionPrompt.nextBreakType === 'long_break'
? 'Start long break'
: 'Start short break'}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={handleContinueSameTask} style={styles.postSessionSecondaryButton}>
<Text style={styles.postSessionSecondaryButtonText}>Continue same task</Text>

View File

@@ -1,7 +1,8 @@
import { GetActiveSession, RemoveActiveSession } from '@/lib/asyncStorage';
import { GetActiveSession } from '@/lib/asyncStorage';
import { formatDateTime } from '@/lib/date';
import { CheckAssignmentCompletion } from '@/lib/progress';
import { DEFAULT_FOCUS_DURATION_MINUTES } from '@/lib/sessionDefaults';
import { finalizeStoredSession } from '@/lib/sessionLifecycle';
import { getSubjectColorSet, type SubjectColor } from '@/lib/subjectColors';
import { supabase } from '@/lib/supabase';
import type { Task } from '@/lib/types';
@@ -151,7 +152,7 @@ const handleSprintStart = async () => {
const secondsLeft = Math.ceil((activeSession.endTime - Date.now()) / 1000)
if (secondsLeft <= 0) {
await RemoveActiveSession();
await finalizeStoredSession('expired', activeSession);
router.push({
pathname: '/task/timer',
params: {
@@ -182,7 +183,7 @@ const handleSprintStart = async () => {
text: 'Start new sprint',
style: 'destructive',
onPress: async () => {
await RemoveActiveSession();
await finalizeStoredSession('cancelled', activeSession);
router.push({
pathname: '/task/timer',
params: {

View File

@@ -3,15 +3,24 @@ import type { SessionType } from '@/lib/types';
const notificationKey = (aId: string) => `assignment_notification_${aId}`;
const activeSprintKey = 'active_sprint';
const studyCycleKey = 'study_cycle';
export type ActiveSession = {
sessionId: string;
sessionType: SessionType;
taskId: string | null;
returnTaskId?: string | null;
durationSeconds: number;
endTime: number;
};
export type StudyCycle = {
taskId: string;
completedFocusSessions: number;
lastCompletedSessionType: SessionType;
lastCompletedAt: number;
};
export async function SaveAssignmentNotificationId(aId: string, notificationId: string) {
await AsyncStorage.setItem(notificationKey(aId), notificationId);
}
@@ -41,3 +50,21 @@ export async function GetActiveSession() {
export async function RemoveActiveSession() {
await AsyncStorage.removeItem(activeSprintKey);
}
export async function SaveStudyCycle(studyCycle: StudyCycle) {
await AsyncStorage.setItem(studyCycleKey, JSON.stringify(studyCycle));
}
export async function GetStudyCycle() {
const studyCycle = await AsyncStorage.getItem(studyCycleKey);
if (!studyCycle) {
return null;
}
return JSON.parse(studyCycle) as StudyCycle;
}
export async function RemoveStudyCycle() {
await AsyncStorage.removeItem(studyCycleKey);
}

View File

@@ -1,2 +1,5 @@
export const DEFAULT_FOCUS_DURATION_MINUTES = 25;
export const DEFAULT_SHORT_BREAK_DURATION_MINUTES = 5;
export const DEFAULT_LONG_BREAK_DURATION_MINUTES = 15;
export const FOCUS_SESSIONS_PER_LONG_BREAK = 4;
export const STUDY_CYCLE_IDLE_RESET_MINUTES = 120;

37
lib/sessionLifecycle.ts Normal file
View File

@@ -0,0 +1,37 @@
import {
GetActiveSession,
RemoveActiveSession,
RemoveStudyCycle,
type ActiveSession,
} from '@/lib/asyncStorage';
import { supabase } from '@/lib/supabase';
export type FinalSessionStatus = 'completed' | 'cancelled' | 'expired';
export async function finalizeStoredSession(
finalStatus: FinalSessionStatus,
activeSessionOverride?: ActiveSession | null
) {
const activeSession = activeSessionOverride ?? await GetActiveSession();
if (!activeSession) {
return null;
}
await RemoveActiveSession();
if (finalStatus !== 'completed') {
await RemoveStudyCycle();
}
const { error } = await supabase.rpc('finalize_sprint_session', {
p_session_id: activeSession.sessionId,
p_final_status: finalStatus,
p_ended_at: new Date().toISOString(),
});
return {
activeSession,
error,
};
}

View File

@@ -0,0 +1,170 @@
# Session Reliability and Break-Cycle Work Report
## #Overview
Today's work continued from the updated vision-gap plan, with the focus narrowed to the remaining timer and session-state gaps rather than broader feature expansion.
The first goal was to finish the last missing part of the focus-and-break loop by making the app distinguish between a short break and a long break in a way that matches an actual study cycle instead of using total historical session count.
After that, the scope shifted into reliability work because the remaining highest-risk issue was not missing UI, but the possibility that local active-session state and recorded session history could drift apart. That led to a review of how the dashboard, setup flow, task details screen, and timer screen each handled expired, cancelled, or replaced sessions.
Later in the same work session, the focus narrowed again into wording and flow polish on the timer screen. The break and sprint descriptions were rewritten so they better reflect the app's goal of supporting structured study behavior, and two runtime regressions reported after testing were fixed in the timer flow itself.
---
## #ImplementedFeatures
### #LongBreakCycleCompletion
Finished the missing long-break part of the focus-and-break loop:
- extended the shared session defaults in `lib/sessionDefaults.ts`
- added:
- `DEFAULT_LONG_BREAK_DURATION_MINUTES`
- `FOCUS_SESSIONS_PER_LONG_BREAK`
- `STUDY_CYCLE_IDLE_RESET_MINUTES`
- updated the timer flow so the next break is chosen from a local study-cycle model instead of total historical session count
The long-break rule now follows a small continuous-cycle interpretation of study flow rather than incorrectly counting unrelated sessions from earlier in the day or from previous days.
---
### #StudyCycleState
Added a small local study-cycle state to support the new break behavior:
- extended `lib/asyncStorage.ts`
- added `StudyCycle`
- added:
- `SaveStudyCycle`
- `GetStudyCycle`
- `RemoveStudyCycle`
- tracked:
- the task tied to the current cycle
- how many focus sessions have been completed in that cycle
- the last completed session type
- the last completion timestamp
This keeps the long-break offer tied to the current study run instead of to the user's entire database history.
---
### #DynamicBreakPrompt
Updated the timer completion overlay so post-focus actions are based on the actual cycle state:
- expanded the post-session prompt model to include the next break type
- replaced the hardcoded short-break action with a dynamic break action
- updated the overlay text and button label so the user now sees:
- `Start short break`
- or `Start long break`
depending on the current cycle
This completes the missing loop from the plan where break behavior should feel intentional rather than unfinished.
---
### #SessionLifecycleConsistency
Introduced a shared finalization path for active sessions:
- added `lib/sessionLifecycle.ts`
- introduced `finalizeStoredSession(...)`
- moved repeated session-finalization behavior into one place:
- remove local active-session state
- clear study-cycle state when the final status is not `completed`
- finalize the same `sessionId` in Supabase with:
- `completed`
- `cancelled`
- `expired`
This reduced the risk that one screen would only clear local storage while another screen properly finalized the database record.
---
### #CrossScreenReliabilityFixes
Applied the shared finalization path across the screens that handle active-session recovery or replacement:
- `app/task/timer.tsx`
- `app/(tabs)/index.tsx`
- `app/task/viewDetailsTask.tsx`
- `app/setup.tsx`
The updated paths now explicitly finalize sessions when they are:
- expired on reopen
- expired while being observed from the dashboard
- cancelled because the user replaces one active sprint with another
This closes a real reliability gap where the app could previously lose the active local session while leaving the recorded session in the database unfinalized.
---
### #TimerAndBreakCopyPolish
Rewrote the timer and break descriptions so they better match the product's intended tone:
- updated the pre-start sprint description
- updated the pre-start break description
- updated the focus fallback description on the running timer
- updated the post-focus and post-break explanation copy
The new wording emphasizes that structured focus and intentional breaks matter for studying, instead of sounding like placeholder or utility-only text.
---
### #ReportedRegressionFixes
Fixed two runtime issues discovered during manual testing after the earlier session-cycle changes:
- after cancelling a focus session, the sprint-duration view could appear visually blank until the user manually dragged the picker
- after completing a long break, `Continue with same task` could route the user back to the dashboard instead of returning to the correct task flow
The fixes were:
- reinitializing the picker-offset path when the timer returns to a non-running state
- preserving `returnTaskId` inside the stored active-session shape so break sessions keep the correct task context all the way through completion
---
## #ProblemsAndSetbacks
### #SessionTruthDivergence
The main reliability issue uncovered today was not in the timer animation itself, but in how different screens treated expired or replaced sessions.
Several screens could detect that a stored session was no longer valid, but some of them only removed the local active-session entry instead of also finalizing the matching `sprint_sessions` row in Supabase. That created a risk where the UI and the database could tell different stories about the same session.
The fix was to stop duplicating that logic screen by screen and route those paths through a shared finalization helper instead.
### #PostChangeRuntimeRegressions
After the cycle and reliability changes landed, manual testing surfaced two smaller regressions in the timer screen:
- the duration screen could look empty after cancelling a focus session
- the break-return flow lost its task target after a long break
These were not architectural problems, but they were both important because they affected the user's immediate understanding of the timer flow after interacting with it.
---
## #CurrentState
The timer and session model are now closer to the final intended behavior in the vision-gap plan.
The app now supports:
- a simple long-break rule tied to the current study cycle
- a local cycle model that avoids counting unrelated older sessions
- a post-focus overlay that correctly offers either a short break or a long break
- a shared session-finalization path used across timer, dashboard, setup, and task-details flows
- better consistency between active local session state and recorded session history
- more intentional sprint and break wording on the timer screen
- preserved task-return context across long-break completion
- corrected timer-screen recovery after cancelling a focus session
At this point, the biggest remaining work in this area is no longer basic feature completion. It is verifying the edge cases that depend on real runtime behavior, such as backgrounding, reopen timing, and cross-screen recovery under actual app usage.
---
## #Verification
Static checks were run after the main implementation work and again after the regression fixes:
```text
npx tsc --noEmit
exited successfully
npm run lint
exited successfully
npx tsc --noEmit
exited successfully
npm run lint
exited successfully
```
Manual testing also confirmed most of the intended behavior from today's scope. Two regressions were found during that testing, both inside the timer flow, and both were fixed in the same work session:
- blank sprint-duration state after cancelling a focus session
- incorrect dashboard return after pressing `Continue with same task` following a long break