diff --git a/electron/src/renderer/pages/DeepSeshPage.tsx b/electron/src/renderer/pages/DeepSeshPage.tsx
index 88a52a4..5bd1eec 100644
--- a/electron/src/renderer/pages/DeepSeshPage.tsx
+++ b/electron/src/renderer/pages/DeepSeshPage.tsx
@@ -1,7 +1,7 @@
// Main Deep Sesh screen shown after onboarding.
// This page composes the Deep Sesh UI and keeps timer display text together.
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import DeepSeshModeSelector from '../components/deepSesh/DeepSeshModeSelector'
import DeepSeshSetupPanel from '../components/deepSesh/DeepSeshSetupPanel'
import DeepSeshTimerCard from '../components/deepSesh/DeepSeshTimerCard'
@@ -13,6 +13,10 @@ import {
import { useDeepSeshTimer } from '../hooks/useDeepSeshTimer'
import type { BrowserActivityPayload } from '../../shared/browserActivity'
import type { DesktopActivityPayload } from '../../shared/focusMonitoring'
+import type {
+ AppRuleStatus,
+ BrowserActivityRuleStatus,
+} from '../hooks/useFocusEnvironmentSettings'
import '../styles/deepSesh.css'
export default function DeepSeshPage() {
@@ -29,6 +33,10 @@ export default function DeepSeshPage() {
browserActivity,
desktopActivity,
})
+ const shouldShowReviewScreen =
+ !timer.isSessionActive &&
+ focusMonitor.hasCompletedSessionSummary &&
+ focusMonitor.unknownActivities.length > 0
const layoutClass = timer.isSessionActive
? 'deep-sesh-screen--active'
: 'deep-sesh-screen--setup'
@@ -167,8 +175,12 @@ export default function DeepSeshPage() {
-
-
+
+
{/* Mode, timer, setup, and summary are split for reviewable UI changes. */}
@@ -216,7 +228,175 @@ export default function DeepSeshPage() {
)}
+
+
+ {shouldShowReviewScreen && (
+
+ )}
+
+
+ )
+}
+
+function SessionReviewPanel({
+ focusMonitor,
+}: {
+ focusMonitor: ReturnType
+}) {
+ const [selectedStatuses, setSelectedStatuses] = useState<
+ Record
+ >({})
+ const selectedCount = Object.keys(selectedStatuses).length
+ const unresolvedCount = Math.max(
+ 0,
+ focusMonitor.unknownActivities.length - selectedCount,
+ )
+ const canContinue = selectedCount > 0
+
+ const sortedUnknownActivities = useMemo(() => {
+ return [...focusMonitor.unknownActivities].sort((leftItem, rightItem) => {
+ return leftItem.kind.localeCompare(rightItem.kind) ||
+ leftItem.label.localeCompare(rightItem.label)
+ })
+ }, [focusMonitor.unknownActivities])
+
+ /* Stores the review choice locally so the user can change their mind before saving. */
+ function selectUnknownStatus(
+ itemId: string,
+ status: AppRuleStatus | BrowserActivityRuleStatus,
+ ) {
+ setSelectedStatuses((currentStatuses) => ({
+ ...currentStatuses,
+ [itemId]: status,
+ }))
+ }
+
+ function doLater() {
+ focusMonitor.dismissSessionSummary({ keepUnknownActivities: true })
+ }
+
+ function continueReview() {
+ if (!canContinue) {
+ return
+ }
+
+ if (
+ unresolvedCount > 0 &&
+ !window.confirm(
+ `${unresolvedCount} unknown item${unresolvedCount === 1 ? '' : 's'} ` +
+ 'still need a choice. Taskmaster will ask you again after a later session.',
+ )
+ ) {
+ return
+ }
+
+ focusMonitor.unknownActivities.forEach((item) => {
+ const selectedStatus = selectedStatuses[item.id]
+
+ if (selectedStatus) {
+ focusMonitor.reviewUnknownActivity(item, selectedStatus)
+ }
+ })
+ focusMonitor.dismissSessionSummary({ keepUnknownActivities: true })
+ }
+
+ return (
+
+
+
+
+
+ Session summary
+
Review new activity
+
+
+
+ Choose what Taskmaster should do with anything new it saw. Unchosen
+ items stay unknown and will come back after a future session.
+
+
+
+
+ Distractions
+ {focusMonitor.stats.distractionEvents}
+
+
+ Distracted time
+ {formatSecondsLabel(focusMonitor.stats.distractedSeconds)}
+
+
+ Unknown
+ {focusMonitor.stats.unknownCount}
+
+
+
+
+ {sortedUnknownActivities.map((item) => {
+ const selectedStatus = selectedStatuses[item.id]
+
+ return (
+
+
+
{item.kind === 'desktop-app' ? 'Unknown app' : 'Unknown page'}
+
{item.label}
+
{item.detail}
+
+
+
+
+
+
+
+ )
+ })}
+
+
+
+
+ {selectedCount === 0
+ ? 'Choose at least one item to continue.'
+ : `${selectedCount} selected, ${unresolvedCount} left for later.`}
+
+
+
)
@@ -296,3 +476,14 @@ function formatDurationLabel(totalMinutes: number) {
return `${hourText} ${minuteText}`
}
+
+function formatSecondsLabel(totalSeconds: number) {
+ const minutes = Math.floor(totalSeconds / 60)
+ const seconds = totalSeconds % 60
+
+ if (minutes === 0) {
+ return `${seconds}s`
+ }
+
+ return `${minutes}m ${seconds}s`
+}
diff --git a/electron/src/renderer/styles/deepSesh.css b/electron/src/renderer/styles/deepSesh.css
index 00fb4ff..4f5019d 100644
--- a/electron/src/renderer/styles/deepSesh.css
+++ b/electron/src/renderer/styles/deepSesh.css
@@ -62,11 +62,13 @@
}
.deep-sesh-shell {
+ position: relative;
display: grid;
width: min(100%, 76rem);
height: 100%;
margin: 0 auto;
padding: clamp(var(--space-md), 3vh, var(--space-xl));
+ overflow: hidden;
}
.deep-sesh-main {
@@ -74,7 +76,16 @@
width: min(100%, 44rem);
min-height: 0;
place-self: center;
- transition: width 420ms cubic-bezier(0.2, 0.9, 0.2, 1);
+ transition:
+ opacity 360ms ease,
+ transform 460ms cubic-bezier(0.2, 0.9, 0.2, 1),
+ width 420ms cubic-bezier(0.2, 0.9, 0.2, 1);
+}
+
+.deep-sesh-shell--reviewing .deep-sesh-main--timer {
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(-4rem) scale(0.96);
}
.deep-sesh-screen--active .deep-sesh-main {
@@ -501,6 +512,135 @@
white-space: nowrap;
}
+.deep-sesh-monitor-rule--allowed {
+ border-color: color-mix(in srgb, #4ade80 42%, var(--color-border));
+}
+
+.deep-sesh-monitor-rule--blocked {
+ border-color: color-mix(in srgb, var(--color-accent-bright) 68%, var(--color-border));
+}
+
+.deep-sesh-monitor-rule--ignored {
+ border-color: color-mix(in srgb, var(--color-accent) 36%, var(--color-border));
+}
+
+.deep-sesh-monitor-rule--unknown {
+ border-color: color-mix(in srgb, var(--color-text-muted) 38%, var(--color-border));
+}
+
+.deep-sesh-review-screen {
+ position: absolute;
+ inset: clamp(var(--space-md), 3vh, var(--space-xl));
+ z-index: 4;
+ display: grid;
+ align-content: center;
+ justify-items: center;
+ opacity: 0;
+ transform: translateX(4rem) scale(0.98);
+ animation: deep-sesh-review-enter 460ms cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
+}
+
+.deep-sesh-review-later-button {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+.deep-sesh-review-panel {
+ display: grid;
+ width: min(100%, 56rem);
+ max-height: min(42rem, calc(100vh - 7rem));
+ gap: var(--space-md);
+ padding: var(--space-lg);
+ overflow: hidden;
+ text-align: left;
+}
+
+.deep-sesh-review-header {
+ display: grid;
+ gap: 0.4rem;
+}
+
+.deep-sesh-review-header h2 {
+ margin: 0;
+ font-size: clamp(1.2rem, 1.4vw, 1.55rem);
+}
+
+.deep-sesh-review-stats {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: var(--space-sm);
+}
+
+.deep-sesh-review-stats div,
+.deep-sesh-review-item {
+ display: grid;
+ gap: 0.18rem;
+ padding: 0.7rem 0.8rem;
+ border: 1px solid color-mix(in srgb, var(--color-border) 66%, transparent);
+ border-radius: var(--radius-sm);
+ background: color-mix(in srgb, var(--color-bg-card) 54%, transparent);
+}
+
+.deep-sesh-review-stats span,
+.deep-sesh-review-item span {
+ color: var(--color-text-muted);
+ font-size: clamp(0.7rem, 0.66vw, 0.78rem);
+ font-weight: 800;
+}
+
+.deep-sesh-review-stats strong,
+.deep-sesh-review-item strong {
+ color: var(--color-text-main);
+}
+
+.deep-sesh-review-list {
+ display: grid;
+ gap: var(--space-sm);
+ min-height: 0;
+ overflow: auto;
+ padding-right: 0.2rem;
+}
+
+.deep-sesh-review-item {
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+}
+
+.deep-sesh-review-item p {
+ margin: 0;
+}
+
+.deep-sesh-review-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+}
+
+.deep-sesh-review-actions .secondary-button {
+ min-height: 2.15rem;
+ padding: 0.42rem 0.7rem;
+}
+
+.deep-sesh-review-choice--active {
+ border-color: var(--color-border-accent);
+ color: #161207;
+ background: var(--color-accent);
+}
+
+.deep-sesh-review-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+ padding-top: var(--space-sm);
+ border-top: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
+}
+
+.deep-sesh-review-footer .primary-button {
+ min-width: 8rem;
+}
+
.deep-sesh-setting-grid {
display: grid;
width: 100%;
@@ -606,6 +746,13 @@
}
}
+@keyframes deep-sesh-review-enter {
+ to {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+}
+
@media (max-height: 760px) {
.deep-sesh-corner-title h1 {
font-size: clamp(1.15rem, 2vw, 1.8rem);
@@ -689,10 +836,29 @@
}
.deep-sesh-setting-grid,
+ .deep-sesh-review-stats,
.deep-sesh-summary-grid {
grid-template-columns: minmax(0, 1fr);
}
+ .deep-sesh-review-screen {
+ inset: 4.8rem var(--space-md) 4.8rem;
+ align-content: stretch;
+ }
+
+ .deep-sesh-review-panel {
+ align-self: center;
+ max-height: 100%;
+ padding: var(--space-md);
+ }
+
+ .deep-sesh-review-item,
+ .deep-sesh-review-footer {
+ grid-template-columns: minmax(0, 1fr);
+ flex-direction: column;
+ align-items: stretch;
+ }
+
.deep-sesh-summary-grid {
display: none;
}