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; }