@nightkatana/kronosys-app 1.0.0-beta.2 → 1.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +28 -1
  2. package/app/api/action/route.ts +39 -3
  3. package/app/api/action-logs/route.ts +24 -0
  4. package/app/api/backup/route.ts +1 -1
  5. package/app/api/restore/route.ts +145 -0
  6. package/app/changelog/page.tsx +71 -4
  7. package/app/globals.css +127 -0
  8. package/app/guide/page.tsx +61 -15
  9. package/app/implementation/page.tsx +700 -0
  10. package/app/layout.tsx +14 -3
  11. package/app/licenses/page.tsx +99 -37
  12. package/app/logs/page.tsx +258 -0
  13. package/app/manifest.ts +5 -5
  14. package/app/page.tsx +784 -229
  15. package/app/reporting/page.tsx +1266 -474
  16. package/app/settings/page.tsx +252 -18
  17. package/bin/kronosys.mjs +140 -15
  18. package/components/KronosysPayloadProvider.tsx +2 -0
  19. package/components/RouteTransition.tsx +18 -0
  20. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
  21. package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
  22. package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
  23. package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
  24. package/components/dashboard/AppShellRouteNav.tsx +323 -48
  25. package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
  26. package/components/dashboard/DashboardSimpleModal.tsx +168 -25
  27. package/components/dashboard/DashboardTour.tsx +115 -29
  28. package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
  29. package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
  30. package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
  31. package/components/dashboard/NewSessionScopeModal.tsx +211 -20
  32. package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
  33. package/components/dashboard/ReportingTour.tsx +87 -21
  34. package/components/dashboard/SavedProjectPicker.tsx +16 -3
  35. package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
  36. package/components/dashboard/SessionListPanel.tsx +327 -44
  37. package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
  38. package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
  39. package/components/dashboard/SettingsTour.tsx +86 -21
  40. package/components/dashboard/TagPills.tsx +14 -1
  41. package/components/dashboard/TaskFocusPanel.tsx +1081 -478
  42. package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
  43. package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
  44. package/components/dashboard/taskFieldStyles.ts +20 -4
  45. package/components/dashboard/useReportingInteractionState.ts +80 -0
  46. package/lib/appShellHeaderClasses.ts +13 -0
  47. package/lib/businessRulesMatrix.ts +210 -0
  48. package/lib/copyToClipboard.ts +43 -0
  49. package/lib/dashboardCopy.ts +494 -84
  50. package/lib/dashboardQuickSearch.ts +54 -2
  51. package/lib/dashboardTimeZone.ts +109 -0
  52. package/lib/formatAppShellWallClock.ts +66 -0
  53. package/lib/formatSessionNameTemplate.ts +141 -0
  54. package/lib/generatedUserChangelog.ts +177 -6
  55. package/lib/globalPausePreview.ts +292 -0
  56. package/lib/implementationNotes.ts +1188 -0
  57. package/lib/kronosysApi.ts +6 -0
  58. package/lib/kronosysDashboardModalGates.ts +24 -0
  59. package/lib/plannedBoundaryAttention.ts +9 -0
  60. package/lib/plannedBoundaryConflict.ts +23 -0
  61. package/lib/reportingAggregate.ts +517 -75
  62. package/lib/reportingMetricHelp.ts +8 -0
  63. package/lib/reportingStrings.ts +37 -3
  64. package/lib/sessionListMerge.ts +4 -0
  65. package/lib/sessionTaskSidebarStats.ts +182 -21
  66. package/lib/settingsCopy.ts +178 -4
  67. package/lib/taskParsing.ts +360 -103
  68. package/lib/taskTemplateDraft.ts +135 -0
  69. package/lib/taskTimelineGantt.ts +265 -0
  70. package/lib/temporalDisplayPlanned.ts +71 -0
  71. package/lib/userGuideCopy.ts +121 -47
  72. package/next.config.ts +7 -0
  73. package/package.json +12 -24
  74. package/server/actionDispatch.ts +1000 -77
  75. package/server/actionTaskSession.ts +337 -24
  76. package/server/db.ts +7 -15
  77. package/server/dbSchema.ts +24 -0
  78. package/server/defaultCfg.ts +5 -0
  79. package/server/gitlabTokenStore.ts +0 -12
  80. package/server/liveHistorySync.ts +53 -0
  81. package/server/mainTimerHydrate.ts +38 -2
  82. package/server/payloadStore.ts +33 -11
  83. package/server/sessionWallHydrate.ts +66 -3
  84. package/server/userActionLog.ts +126 -0
  85. package/sonar-project.properties +11 -0
  86. package/tsconfig.json +2 -1
  87. package/components/dashboard/IssuePickerModal.tsx +0 -168
  88. package/components/dashboard/ThemeToggle.test.tsx +0 -26
  89. package/lib/backupCsvExport.test.ts +0 -149
  90. package/lib/dashboardQuickSearchQuery.test.ts +0 -63
  91. package/lib/dataDir.test.ts +0 -87
  92. package/lib/formatIsoShort.test.ts +0 -46
  93. package/lib/kronoFocusRhythm.test.ts +0 -130
  94. package/lib/kronoFocusTimerUrgency.test.ts +0 -74
  95. package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
  96. package/lib/reportingAggregate.test.ts +0 -325
  97. package/lib/reportingNonFinalIndicators.test.ts +0 -157
  98. package/lib/reportingTagWeekBreakdown.test.ts +0 -141
  99. package/lib/reportingWeekLayout.test.ts +0 -239
  100. package/lib/sessionAssiduity.test.ts +0 -25
  101. package/lib/sessionEndWarnings.test.ts +0 -200
  102. package/lib/sessionListMerge.test.ts +0 -101
  103. package/lib/sessionTaskSidebarStats.test.ts +0 -24
  104. package/lib/taskParsing.test.ts +0 -153
  105. package/lib/usageProfile.test.ts +0 -84
  106. package/server/actionDispatch.test.ts +0 -723
  107. package/server/actionTaskSession.test.ts +0 -713
  108. package/server/kronoFocusHydrate.test.ts +0 -142
  109. package/server/kronoFocusMigrate.test.ts +0 -53
  110. package/server/mainTimerHydrate.test.ts +0 -65
  111. package/server/payloadStore.test.ts +0 -78
  112. package/server/sessionWallHydrate.test.ts +0 -46
package/app/page.tsx CHANGED
@@ -10,16 +10,20 @@ import {
10
10
  useState,
11
11
  } from "react";
12
12
  import { usePathname, useRouter, useSearchParams } from "next/navigation";
13
- import { Clock, FolderOpen, Globe, Plus, User, X } from "lucide-react";
14
- import { HeaderIntegrationBadges } from "@/components/dashboard/HeaderIntegrationBadges";
13
+ import { Plus, X } from "lucide-react";
14
+ import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
15
+ import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
15
16
  import {
16
17
  postKronosysAction,
17
18
  type GitRepoStatisticsPayload,
18
19
  type KronosysUpdatePayload,
19
- type WorkspaceCodeSnapshotPayload,
20
20
  } from "@/lib/kronosysApi";
21
21
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
22
- import { appShellHeaderClassName } from "@/lib/appShellHeaderClasses";
22
+ import {
23
+ appShellHeaderClassName,
24
+ appShellHeaderTitleMetaRowClassName,
25
+ appShellHeaderToolbarClassName,
26
+ } from "@/lib/appShellHeaderClasses";
23
27
  import { dashboardColumnTitleRowClassName } from "@/lib/dashboardColumnChrome";
24
28
  import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
25
29
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
@@ -63,6 +67,7 @@ import { SettingsTagsProjectsSection } from "@/components/dashboard/SettingsTags
63
67
  import { InlineMetricHelpTrigger } from "@/components/dashboard/InlineMetricHelpTrigger";
64
68
  import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
65
69
  import { SelectedSessionSidebarBlock } from "@/components/dashboard/SelectedSessionSidebarBlock";
70
+ import { DashboardPauseBackdrop } from "@/components/dashboard/DashboardPauseBackdrop";
66
71
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
67
72
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
68
73
  import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
@@ -77,25 +82,51 @@ import { DashboardLangGateModal } from "@/components/dashboard/DashboardLangGate
77
82
  import { GitIdentityQuickSetupModal } from "@/components/dashboard/GitIdentityQuickSetupModal";
78
83
  import { DashboardTour } from "@/components/dashboard/DashboardTour";
79
84
  import { isDashboardTourCompleted } from "@/lib/dashboardTourStorage";
80
- import { workspaceFolderPathStrings } from "@/lib/legacyEditorPayloadKeys";
81
85
  import { mergeLiveSessionIntoHistory } from "@/lib/sessionListMerge";
86
+ import {
87
+ buildTaskTimelineGanttRows,
88
+ mergeSessionTasksForTimeline,
89
+ parseSessionBoundsMs,
90
+ } from "@/lib/taskTimelineGantt";
82
91
  import {
83
92
  showIdeLinkedCodeTimingMetrics,
84
- showWorkspaceFoldersEmptyMessage,
85
93
  trackCodeMetricsFromCfg,
86
94
  } from "@/lib/usageProfile";
87
95
  import { buildDashboardQuickSearchItems } from "@/lib/dashboardQuickSearch";
96
+ import { parseTaskTemplatesFromPayload } from "@/lib/taskTemplateDraft";
88
97
  import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
89
- import { readDashboardTimeZoneFromCfg } from "@/lib/dashboardTimeZone";
98
+ import {
99
+ calendarDateKeyInTimeZone,
100
+ readDashboardTimeZoneFromCfg,
101
+ } from "@/lib/dashboardTimeZone";
90
102
  import { NewSessionScopeModal } from "@/components/dashboard/NewSessionScopeModal";
103
+ import { GlobalPauseConfirmModal } from "@/components/dashboard/GlobalPauseConfirmModal";
104
+ import { TaskTimelineGanttModal } from "@/components/dashboard/TaskTimelineGanttModal";
91
105
  import { useKronosysPackageVersion } from "@/components/KronosysPackageVersionProvider";
106
+ import {
107
+ buildGlobalPauseActivationPreview,
108
+ isGlobalPauseActivationNoOp,
109
+ } from "@/lib/globalPausePreview";
92
110
 
93
111
  type LiveTaskShape = {
94
112
  id: string;
95
113
  name?: string;
114
+ startTime?: string;
115
+ endTime?: string;
116
+ durationMs?: number;
117
+ taskTimerLaps?: Array<{
118
+ startTime?: string;
119
+ endTime?: string;
120
+ durationMs?: number;
121
+ }>;
122
+ project?: string | null;
96
123
  isDone?: boolean;
97
124
  manualTaskTimerPaused?: boolean;
98
- subtasks?: Array<{ done?: boolean }>;
125
+ subtasks?: Array<{ id?: string; durationMs?: number; done?: boolean }>;
126
+ activeSubtaskTimerId?: string | null;
127
+ subtaskTimerStartedAt?: string | number;
128
+ mainTimerSegmentStartedAt?: string | null;
129
+ taskCurrentLapStartedAt?: string | number | null;
99
130
  };
100
131
 
101
132
  type LiveShape = {
@@ -193,13 +224,35 @@ function DashboardHome() {
193
224
  const [archiveConfirmSessionId, setArchiveConfirmSessionId] = useState<
194
225
  string | null
195
226
  >(null);
227
+ /** Instantané pour animer la disparition dans la liste tant que le serveur n’a plus cette session dans l’historique. */
228
+ const [archiveListExitSession, setArchiveListExitSession] =
229
+ useState<SessionListEntry | null>(null);
230
+ /** Instantané de la session live pour animer la sortie une fois l’état live effacé côté serveur. */
231
+ const [endLiveListExitSession, setEndLiveListExitSession] =
232
+ useState<SessionListEntry | null>(null);
196
233
  const [archiveDismissChecked, setArchiveDismissChecked] = useState(false);
197
234
  const [endLiveSessionConfirmFlags, setEndLiveSessionConfirmFlags] =
198
235
  useState<EndLiveSessionWarningFlags | null>(null);
199
236
  const [endSessionReasonKind, setEndSessionReasonKind] = useState<string>("");
200
237
  const [endSessionReasonNote, setEndSessionReasonNote] = useState("");
238
+ const [endSessionTaskHandling, setEndSessionTaskHandling] = useState<
239
+ "keep" | "finish" | "moveToPausedSession"
240
+ >("keep");
201
241
  const [tourOpen, setTourOpen] = useState(false);
202
242
  const [newSessionModalOpen, setNewSessionModalOpen] = useState(false);
243
+ const pendingNewSessionFocusRef = useRef(false);
244
+ const pendingNewSessionAfterEndRef = useRef<{
245
+ sessionScope: unknown;
246
+ sessionStartAtIso: string | null;
247
+ sessionEndAtIso: string | null;
248
+ } | null>(null);
249
+ const createNewSessionAndFocusRef = useRef<
250
+ (args: {
251
+ sessionScope: unknown;
252
+ sessionStartAtIso: string | null;
253
+ sessionEndAtIso: string | null;
254
+ }) => Promise<void>
255
+ >(async () => {});
203
256
  const [gitBannerDismissed, setGitBannerDismissed] = useState(false);
204
257
  const [gitIdentitySetupModalOpen, setGitIdentitySetupModalOpen] =
205
258
  useState(false);
@@ -207,6 +260,12 @@ function DashboardHome() {
207
260
  useState(false);
208
261
  const [updateChangelogModalOpen, setUpdateChangelogModalOpen] =
209
262
  useState(false);
263
+ const [globalPauseConfirmOpen, setGlobalPauseConfirmOpen] = useState(false);
264
+ const [ganttForSessionId, setGanttForSessionId] = useState<string | null>(
265
+ null,
266
+ );
267
+ const [ganttModalOpen, setGanttModalOpen] = useState(false);
268
+ const [todayGanttModalOpen, setTodayGanttModalOpen] = useState(false);
210
269
  const packageVersion = useKronosysPackageVersion();
211
270
 
212
271
  useEffect(() => {
@@ -252,6 +311,70 @@ function DashboardHome() {
252
311
  const raw = (payload.history || []) as SessionListEntry[];
253
312
  return mergeLiveSessionIntoHistory(raw, live);
254
313
  }, [payload, live]);
314
+
315
+ const liveEndedForExitAnimation =
316
+ typeof endLiveListExitSession?.sessionId === "string" &&
317
+ endLiveListExitSession.sessionId.trim() !== "" &&
318
+ (typeof live?.sessionId !== "string" ||
319
+ live.sessionId.trim() !== endLiveListExitSession.sessionId);
320
+
321
+ const sessionListPanelSessions = useMemo(() => {
322
+ if (archiveListExitSession) {
323
+ const aid = archiveListExitSession.sessionId;
324
+ if (!history.some((s) => s.sessionId === aid)) {
325
+ return [...history, archiveListExitSession];
326
+ }
327
+ }
328
+
329
+ if (
330
+ endLiveListExitSession &&
331
+ liveEndedForExitAnimation &&
332
+ history.some((s) => s.sessionId === endLiveListExitSession.sessionId)
333
+ ) {
334
+ const eid = endLiveListExitSession.sessionId;
335
+ return [
336
+ endLiveListExitSession,
337
+ ...history.filter((s) => s.sessionId !== eid),
338
+ ];
339
+ }
340
+
341
+ return history;
342
+ }, [
343
+ history,
344
+ archiveListExitSession,
345
+ endLiveListExitSession,
346
+ liveEndedForExitAnimation,
347
+ ]);
348
+
349
+ const archiveListExitAnimateId = useMemo(() => {
350
+ if (!archiveListExitSession) {
351
+ return null;
352
+ }
353
+ const id = archiveListExitSession.sessionId;
354
+ if (history.some((s) => s.sessionId === id)) {
355
+ return null;
356
+ }
357
+ return id;
358
+ }, [history, archiveListExitSession]);
359
+
360
+ const endLiveListExitAnimateId = useMemo(() => {
361
+ if (!endLiveListExitSession || !liveEndedForExitAnimation) {
362
+ return null;
363
+ }
364
+ const id = endLiveListExitSession.sessionId;
365
+ if (!history.some((s) => s.sessionId === id)) {
366
+ return null;
367
+ }
368
+ return id;
369
+ }, [history, endLiveListExitSession, liveEndedForExitAnimation]);
370
+
371
+ const sessionRowExitAnimateId =
372
+ archiveListExitAnimateId ?? endLiveListExitAnimateId ?? null;
373
+
374
+ const clearSessionRowExitStates = useCallback(() => {
375
+ setArchiveListExitSession(null);
376
+ setEndLiveListExitSession(null);
377
+ }, []);
255
378
  const urlResolution = resolveUrlSession(
256
379
  urlParam,
257
380
  payload,
@@ -279,7 +402,7 @@ function DashboardHome() {
279
402
  const columnArchiveId =
280
403
  isDetachedUrlTab && urlResolution.id !== live?.sessionId
281
404
  ? urlResolution.id
282
- : (inspectingId ?? null);
405
+ : inspectingId ?? null;
283
406
  const viewingSession = columnArchiveId
284
407
  ? ((history.find((s) => s.sessionId === columnArchiveId) ||
285
408
  historyArchived.find((s) => s.sessionId === columnArchiveId)) as
@@ -288,6 +411,15 @@ function DashboardHome() {
288
411
  : undefined;
289
412
  const sessionCurrent = (viewingSession ?? live) as LiveShape | undefined;
290
413
  const isInspecting = Boolean(columnArchiveId);
414
+ const archiveSessionInMainHistoryList = useMemo(
415
+ () =>
416
+ typeof columnArchiveId === "string" &&
417
+ columnArchiveId.length > 0 &&
418
+ sessionListPanelSessions.some((s) => s.sessionId === columnArchiveId),
419
+ [columnArchiveId, sessionListPanelSessions],
420
+ );
421
+ const showSessionSidebarAboveSessionList =
422
+ !columnArchiveId || !archiveSessionInMainHistoryList;
291
423
 
292
424
  useEffect(() => {
293
425
  if (!payload) {
@@ -463,6 +595,19 @@ function DashboardHome() {
463
595
  const post = useCallback(
464
596
  async (body: Record<string, unknown>) => {
465
597
  const res = await postKronosysAction(body);
598
+ if (!res.ok) {
599
+ const raw =
600
+ typeof res.result?.newSessionError === "string"
601
+ ? res.result.newSessionError.trim()
602
+ : "";
603
+ setHomeDialogAlert(
604
+ raw ||
605
+ (lang === "fr"
606
+ ? "L’action n’a pas pu être appliquée."
607
+ : "The action could not be applied."),
608
+ );
609
+ return res;
610
+ }
466
611
  await refresh();
467
612
  const op = res.result?.sessionOp;
468
613
  if (op && !op.ok) {
@@ -475,8 +620,57 @@ function DashboardHome() {
475
620
  }
476
621
  return res;
477
622
  },
478
- [refresh],
623
+ [lang, refresh],
624
+ );
625
+ const globalPauseContextActive = Boolean(
626
+ live &&
627
+ typeof live === "object" &&
628
+ "globalPauseContext" in live &&
629
+ (live as { globalPauseContext?: unknown }).globalPauseContext,
630
+ );
631
+ const globalPauseActivationPreview = useMemo(
632
+ () =>
633
+ live && typeof live === "object"
634
+ ? buildGlobalPauseActivationPreview(live as Record<string, unknown>)
635
+ : null,
636
+ [live],
479
637
  );
638
+ const globalPauseNavDisabled = useMemo(
639
+ () =>
640
+ !globalPauseContextActive &&
641
+ globalPauseActivationPreview !== null &&
642
+ isGlobalPauseActivationNoOp(globalPauseActivationPreview),
643
+ [globalPauseContextActive, globalPauseActivationPreview],
644
+ );
645
+ const onGlobalPauseNavPress = useCallback(() => {
646
+ if (globalPauseContextActive) {
647
+ void post({ type: "toggleGlobalPauseContext" });
648
+ return;
649
+ }
650
+ if (globalPauseNavDisabled) {
651
+ return;
652
+ }
653
+ setGlobalPauseConfirmOpen(true);
654
+ }, [globalPauseContextActive, globalPauseNavDisabled, post]);
655
+ const confirmGlobalPauseActivation = useCallback(() => {
656
+ setGlobalPauseConfirmOpen(false);
657
+ void post({ type: "toggleGlobalPauseContext" });
658
+ }, [post]);
659
+ const cancelGlobalPauseConfirm = useCallback(() => {
660
+ setGlobalPauseConfirmOpen(false);
661
+ }, []);
662
+
663
+ useEffect(() => {
664
+ if (globalPauseContextActive) {
665
+ setGlobalPauseConfirmOpen(false);
666
+ }
667
+ }, [globalPauseContextActive]);
668
+
669
+ useEffect(() => {
670
+ if (!live) {
671
+ setGlobalPauseConfirmOpen(false);
672
+ }
673
+ }, [live]);
480
674
 
481
675
  const completeLangGate = useCallback(
482
676
  (next: Lang) => {
@@ -504,10 +698,10 @@ function DashboardHome() {
504
698
  p.error === "disabled"
505
699
  ? dt.sessionMongoPushFailedDisabled
506
700
  : p.error === "not_found"
507
- ? dt.sessionMongoPushFailedNotFound
508
- : p.error === "uri_incomplete"
509
- ? dt.sessionMongoPushFailedUri
510
- : dt.sessionMongoPushFailedMongo;
701
+ ? dt.sessionMongoPushFailedNotFound
702
+ : p.error === "uri_incomplete"
703
+ ? dt.sessionMongoPushFailedUri
704
+ : dt.sessionMongoPushFailedMongo;
511
705
  setHomeDialogAlert(msg);
512
706
  }
513
707
  } catch {
@@ -532,12 +726,16 @@ function DashboardHome() {
532
726
  const confirmArchiveSession = useCallback(
533
727
  (sessionId: string) => {
534
728
  if (payload?.dismissArchiveSessionConfirm === true) {
729
+ const snap = history.find((s) => s.sessionId === sessionId);
730
+ if (snap) {
731
+ setArchiveListExitSession(snap);
732
+ }
535
733
  void post({ type: "archiveSession", sessionId, archived: true });
536
734
  return;
537
735
  }
538
736
  setArchiveConfirmSessionId(sessionId);
539
737
  },
540
- [payload?.dismissArchiveSessionConfirm, post],
738
+ [history, payload?.dismissArchiveSessionConfirm, post],
541
739
  );
542
740
 
543
741
  const handleDeleteConfirm = useCallback(
@@ -566,6 +764,7 @@ function DashboardHome() {
566
764
  const flags = getEndLiveSessionWarningFlags(live);
567
765
  setEndSessionReasonKind("");
568
766
  setEndSessionReasonNote("");
767
+ setEndSessionTaskHandling("keep");
569
768
  setEndLiveSessionConfirmFlags(flags);
570
769
  }, [live]);
571
770
 
@@ -631,15 +830,28 @@ function DashboardHome() {
631
830
  setEndLiveSessionConfirmFlags(null);
632
831
  setEndSessionReasonKind("");
633
832
  setEndSessionReasonNote("");
833
+ setEndSessionTaskHandling("keep");
834
+ pendingNewSessionAfterEndRef.current = null;
634
835
  }, []);
635
836
 
636
- const confirmEndLiveSession = useCallback(() => {
837
+ const confirmEndLiveSession = useCallback(async () => {
637
838
  const kind = endSessionReasonKind;
638
839
  const note = endSessionReasonNote.trim();
840
+ const handling = endSessionTaskHandling;
841
+ const liveSidRaw =
842
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
843
+ const snap =
844
+ liveSidRaw !== ""
845
+ ? history.find((s) => s.sessionId === liveSidRaw)
846
+ : undefined;
639
847
  setEndLiveSessionConfirmFlags(null);
640
848
  setEndSessionReasonKind("");
641
849
  setEndSessionReasonNote("");
642
- void post({
850
+ setEndSessionTaskHandling("keep");
851
+ if (snap) {
852
+ setEndLiveListExitSession(snap);
853
+ }
854
+ await post({
643
855
  type: "endLiveSession",
644
856
  ...(kind === "planned" ||
645
857
  kind === "early" ||
@@ -648,8 +860,21 @@ function DashboardHome() {
648
860
  ? { sessionEndReasonKind: kind }
649
861
  : {}),
650
862
  ...(note.length > 0 ? { sessionEndReasonNote: note } : {}),
863
+ ...(handling !== "keep" ? { taskHandling: handling } : {}),
651
864
  });
652
- }, [post, endSessionReasonKind, endSessionReasonNote]);
865
+ const pend = pendingNewSessionAfterEndRef.current;
866
+ pendingNewSessionAfterEndRef.current = null;
867
+ if (pend) {
868
+ await createNewSessionAndFocusRef.current(pend);
869
+ }
870
+ }, [
871
+ history,
872
+ live?.sessionId,
873
+ post,
874
+ endSessionReasonKind,
875
+ endSessionReasonNote,
876
+ endSessionTaskHandling,
877
+ ]);
653
878
 
654
879
  const handleSelectSession = useCallback(
655
880
  async (sessionId: string) => {
@@ -709,6 +934,32 @@ function DashboardHome() {
709
934
  });
710
935
  }, []);
711
936
 
937
+ const focusTaskLauncherInput = useCallback(() => {
938
+ document.getElementById("dashboard-col-tasks")?.scrollIntoView({
939
+ behavior: "smooth",
940
+ block: "start",
941
+ });
942
+ const tryFocus = () => {
943
+ const input = document.getElementById(
944
+ "kronosys-task-launcher-input",
945
+ ) as HTMLInputElement | null;
946
+ if (!input) {
947
+ return false;
948
+ }
949
+ input.focus();
950
+ input.select();
951
+ return true;
952
+ };
953
+ globalThis.requestAnimationFrame(() => {
954
+ if (tryFocus()) {
955
+ return;
956
+ }
957
+ globalThis.setTimeout(() => {
958
+ void tryFocus();
959
+ }, 140);
960
+ });
961
+ }, []);
962
+
712
963
  const scrollToTaskInPanel = useCallback((taskId: string) => {
713
964
  const el =
714
965
  document.getElementById(`kronosys-active-task-${taskId}`) ??
@@ -716,6 +967,18 @@ function DashboardHome() {
716
967
  el?.scrollIntoView({ behavior: "smooth", block: "nearest" });
717
968
  }, []);
718
969
 
970
+ const taskLauncherApplyDraftRef = useRef<((text: string) => void) | null>(
971
+ null,
972
+ );
973
+
974
+ const taskTemplatesForSearch = useMemo(
975
+ () =>
976
+ parseTaskTemplatesFromPayload(
977
+ (payload as Record<string, unknown> | undefined)?.taskTemplates,
978
+ ),
979
+ [payload],
980
+ );
981
+
719
982
  const sessionDurationAlertThresholdMinutes = useMemo(() => {
720
983
  const raw = (payload?.cfg as Record<string, unknown> | undefined)
721
984
  ?.dashboardSessionDurationAlertHours;
@@ -761,14 +1024,195 @@ function DashboardHome() {
761
1024
  }, [payload?.cfg]);
762
1025
 
763
1026
  const dashboardDisplayTimeZone = useMemo(
764
- () => readDashboardTimeZoneFromCfg(payload?.cfg as Record<string, unknown> | undefined),
765
- [payload?.cfg]
1027
+ () =>
1028
+ readDashboardTimeZoneFromCfg(
1029
+ payload?.cfg as Record<string, unknown> | undefined,
1030
+ ),
1031
+ [payload?.cfg],
766
1032
  );
767
1033
  const dashboardUse24HourClock = useMemo(
768
- () => readDashboardUse24HourClockFromCfg(payload?.cfg as Record<string, unknown> | undefined),
769
- [payload?.cfg]
1034
+ () =>
1035
+ readDashboardUse24HourClockFromCfg(
1036
+ payload?.cfg as Record<string, unknown> | undefined,
1037
+ ),
1038
+ [payload?.cfg],
1039
+ );
1040
+
1041
+ const openGanttForSessionId = useCallback((sessionId: string) => {
1042
+ const id = sessionId.trim();
1043
+ if (!id) {
1044
+ return;
1045
+ }
1046
+ setTodayGanttModalOpen(false);
1047
+ setGanttForSessionId(id);
1048
+ setGanttModalOpen(true);
1049
+ }, []);
1050
+
1051
+ const closeGanttModal = useCallback(() => {
1052
+ setGanttModalOpen(false);
1053
+ setGanttForSessionId(null);
1054
+ }, []);
1055
+
1056
+ const openTodayGanttModal = useCallback(() => {
1057
+ setGanttModalOpen(false);
1058
+ setGanttForSessionId(null);
1059
+ setTodayGanttModalOpen(true);
1060
+ }, []);
1061
+
1062
+ const closeTodayGanttModal = useCallback(() => {
1063
+ setTodayGanttModalOpen(false);
1064
+ }, []);
1065
+
1066
+ const ganttResolvedSession = useMemo(():
1067
+ | SessionListEntry
1068
+ | LiveShape
1069
+ | null => {
1070
+ const focus = ganttForSessionId?.trim();
1071
+ if (!focus) {
1072
+ return null;
1073
+ }
1074
+ const liveSid =
1075
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
1076
+ if (liveSid !== "" && liveSid === focus) {
1077
+ return live as LiveShape;
1078
+ }
1079
+ return (
1080
+ history.find((s) => s.sessionId === focus) ??
1081
+ historyArchived.find((s) => s.sessionId === focus) ??
1082
+ null
1083
+ );
1084
+ }, [ganttForSessionId, live, history, historyArchived]);
1085
+
1086
+ const ganttIsInspecting = useMemo(() => {
1087
+ const focus = ganttForSessionId?.trim() ?? "";
1088
+ const liveSid =
1089
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
1090
+ return liveSid === "" || focus !== liveSid;
1091
+ }, [ganttForSessionId, live?.sessionId]);
1092
+
1093
+ const ganttTimelineRows = useMemo(() => {
1094
+ if (!ganttResolvedSession) {
1095
+ return [];
1096
+ }
1097
+ return buildTaskTimelineGanttRows(
1098
+ mergeSessionTasksForTimeline(ganttResolvedSession),
1099
+ {
1100
+ isInspecting: ganttIsInspecting,
1101
+ lang,
1102
+ displayTimeZone: dashboardDisplayTimeZone,
1103
+ use24HourClock: dashboardUse24HourClock,
1104
+ },
1105
+ );
1106
+ }, [
1107
+ ganttResolvedSession,
1108
+ ganttIsInspecting,
1109
+ lang,
1110
+ dashboardDisplayTimeZone,
1111
+ dashboardUse24HourClock,
1112
+ ]);
1113
+
1114
+ const {
1115
+ sessionStartMs: ganttSessionStartMs,
1116
+ sessionEndMs: ganttSessionEndMs,
1117
+ } = useMemo(
1118
+ () => parseSessionBoundsMs(ganttResolvedSession ?? undefined),
1119
+ [ganttResolvedSession],
770
1120
  );
771
1121
 
1122
+ const todayGanttTimelineRows = useMemo(() => {
1123
+ const dayKey = calendarDateKeyInTimeZone(
1124
+ new Date().toISOString(),
1125
+ dashboardDisplayTimeZone,
1126
+ );
1127
+ if (!dayKey) {
1128
+ return [];
1129
+ }
1130
+ const sessionMap = new Map<string, SessionListEntry | LiveShape>();
1131
+ const liveSid =
1132
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
1133
+ if (liveSid !== "") {
1134
+ sessionMap.set(liveSid, live as LiveShape);
1135
+ }
1136
+ for (const session of history) {
1137
+ const sid = session.sessionId?.trim();
1138
+ if (sid) {
1139
+ sessionMap.set(sid, session);
1140
+ }
1141
+ }
1142
+ for (const session of historyArchived) {
1143
+ const sid = session.sessionId?.trim();
1144
+ if (sid && !sessionMap.has(sid)) {
1145
+ sessionMap.set(sid, session);
1146
+ }
1147
+ }
1148
+
1149
+ const mergedTasks: Array<{
1150
+ id: string;
1151
+ name?: string;
1152
+ startTime?: string;
1153
+ endTime?: string;
1154
+ durationMs?: number;
1155
+ project?: string | null;
1156
+ }> = [];
1157
+
1158
+ for (const [sid, session] of sessionMap.entries()) {
1159
+ const tasks = mergeSessionTasksForTimeline(session);
1160
+ for (const task of tasks) {
1161
+ const startIso =
1162
+ typeof task.startTime === "string" ? task.startTime : null;
1163
+ const endIso = typeof task.endTime === "string" ? task.endTime : null;
1164
+ const startDay = calendarDateKeyInTimeZone(
1165
+ startIso,
1166
+ dashboardDisplayTimeZone,
1167
+ );
1168
+ const endDay = calendarDateKeyInTimeZone(
1169
+ endIso,
1170
+ dashboardDisplayTimeZone,
1171
+ );
1172
+ const includeToday =
1173
+ startDay === dayKey ||
1174
+ endDay === dayKey ||
1175
+ (endIso === null &&
1176
+ startDay !== null &&
1177
+ startDay <= dayKey &&
1178
+ liveSid === sid);
1179
+ if (!includeToday) {
1180
+ continue;
1181
+ }
1182
+ const taskId =
1183
+ typeof task.id === "string" && task.id.trim() !== ""
1184
+ ? task.id.trim()
1185
+ : `task-${mergedTasks.length + 1}`;
1186
+ mergedTasks.push({
1187
+ id: `${sid}:${taskId}`,
1188
+ name: task.name,
1189
+ startTime: startIso ?? undefined,
1190
+ endTime: endIso ?? undefined,
1191
+ durationMs:
1192
+ typeof task.durationMs === "number" ? task.durationMs : undefined,
1193
+ project:
1194
+ typeof task.project === "string" || task.project === null
1195
+ ? task.project
1196
+ : null,
1197
+ });
1198
+ }
1199
+ }
1200
+
1201
+ return buildTaskTimelineGanttRows(mergedTasks, {
1202
+ isInspecting: false,
1203
+ lang,
1204
+ displayTimeZone: dashboardDisplayTimeZone,
1205
+ use24HourClock: dashboardUse24HourClock,
1206
+ });
1207
+ }, [
1208
+ dashboardDisplayTimeZone,
1209
+ dashboardUse24HourClock,
1210
+ history,
1211
+ historyArchived,
1212
+ lang,
1213
+ live,
1214
+ ]);
1215
+
772
1216
  const quickSearchItems = useMemo(
773
1217
  () =>
774
1218
  buildDashboardQuickSearchItems({
@@ -794,6 +1238,13 @@ function DashboardHome() {
794
1238
  scrollToSession: scrollToSessionInList,
795
1239
  focusTasksColumn: focusTasksColumnForSearch,
796
1240
  scrollToTask: scrollToTaskInPanel,
1241
+ taskTemplates: taskTemplatesForSearch,
1242
+ onSelectTaskTemplate: (draft) => {
1243
+ focusTasksColumnForSearch();
1244
+ globalThis.requestAnimationFrame(() => {
1245
+ taskLauncherApplyDraftRef.current?.(draft);
1246
+ });
1247
+ },
797
1248
  }),
798
1249
  [
799
1250
  dt,
@@ -805,6 +1256,7 @@ function DashboardHome() {
805
1256
  scrollToSessionInList,
806
1257
  scrollToTaskInPanel,
807
1258
  sessionCurrent,
1259
+ taskTemplatesForSearch,
808
1260
  ],
809
1261
  );
810
1262
 
@@ -823,9 +1275,79 @@ function DashboardHome() {
823
1275
  await refresh();
824
1276
  }, [pathname, refresh, router, searchParams, sessionQueryMode]);
825
1277
 
1278
+ useEffect(() => {
1279
+ if (!pendingNewSessionFocusRef.current) {
1280
+ return;
1281
+ }
1282
+ const liveSid =
1283
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
1284
+ if (!liveSid) {
1285
+ return;
1286
+ }
1287
+ pendingNewSessionFocusRef.current = false;
1288
+ focusTaskLauncherInput();
1289
+ }, [focusTaskLauncherInput, live?.sessionId]);
1290
+
1291
+ const createNewSessionAndFocus = useCallback(
1292
+ async (args: {
1293
+ sessionScope: unknown;
1294
+ sessionStartAtIso: string | null;
1295
+ sessionEndAtIso: string | null;
1296
+ }) => {
1297
+ setNewSessionModalOpen(false);
1298
+ const res = await post({
1299
+ type: "newSession",
1300
+ sessionScope: args.sessionScope,
1301
+ ...(args.sessionStartAtIso
1302
+ ? { sessionStartAt: args.sessionStartAtIso }
1303
+ : {}),
1304
+ ...(args.sessionEndAtIso ? { sessionEndAt: args.sessionEndAtIso } : {}),
1305
+ });
1306
+ if (!res.ok) {
1307
+ return;
1308
+ }
1309
+ const hid =
1310
+ typeof res.result?.newHistorySessionId === "string"
1311
+ ? res.result.newHistorySessionId.trim()
1312
+ : "";
1313
+ if (hid !== "") {
1314
+ pendingNewSessionFocusRef.current = false;
1315
+ await handleSelectSession(hid);
1316
+ scrollToSessionInList(hid);
1317
+ focusTaskLauncherInput();
1318
+ return;
1319
+ }
1320
+ pendingNewSessionFocusRef.current = true;
1321
+ if (sessionQueryMode) {
1322
+ router.replace(
1323
+ pathnameWithUpdatedSessionQuery(
1324
+ pathname,
1325
+ searchParams.toString(),
1326
+ null,
1327
+ ),
1328
+ );
1329
+ }
1330
+ focusTaskLauncherInput();
1331
+ },
1332
+ [
1333
+ focusTaskLauncherInput,
1334
+ handleSelectSession,
1335
+ pathname,
1336
+ post,
1337
+ router,
1338
+ scrollToSessionInList,
1339
+ searchParams,
1340
+ sessionQueryMode,
1341
+ ],
1342
+ );
1343
+
1344
+ createNewSessionAndFocusRef.current = createNewSessionAndFocus;
1345
+
826
1346
  const openSessionInNewTab = useCallback(
827
1347
  (sessionId: string) => {
828
- const url = `${globalThis.location.origin}${pathname}?session=${encodeURIComponent(sessionId)}`;
1348
+ const url = `${
1349
+ globalThis.location.origin
1350
+ }${pathname}?session=${encodeURIComponent(sessionId)}`;
829
1351
  globalThis.open(url, "_blank", "noopener,noreferrer");
830
1352
  },
831
1353
  [pathname],
@@ -835,10 +1357,10 @@ function DashboardHome() {
835
1357
  urlResolution.mode === "ok"
836
1358
  ? urlResolution.id
837
1359
  : urlResolution.mode === "loading" && urlParam
838
- ? urlParam
839
- : urlResolution.mode === "invalid"
840
- ? live?.sessionId
841
- : (inspectingId ?? live?.sessionId);
1360
+ ? urlParam
1361
+ : urlResolution.mode === "invalid"
1362
+ ? live?.sessionId
1363
+ : inspectingId ?? live?.sessionId;
842
1364
 
843
1365
  const trimmedSelectedSessionId =
844
1366
  typeof selectedSessionId === "string" ? selectedSessionId.trim() : "";
@@ -858,13 +1380,6 @@ function DashboardHome() {
858
1380
  typeof gitIdentity?.gitAccountLogin === "string"
859
1381
  ? gitIdentity.gitAccountLogin.trim()
860
1382
  : "";
861
- const gitContextLine = [
862
- gitUserName || null,
863
- gitUserEmail || null,
864
- gitAccountLogin || null,
865
- ]
866
- .filter(Boolean)
867
- .join(" · ");
868
1383
  const hasGitIdentityConfigured = Boolean(
869
1384
  gitUserName || gitUserEmail || gitAccountLogin,
870
1385
  );
@@ -925,56 +1440,58 @@ function DashboardHome() {
925
1440
  : "sqlite";
926
1441
  const mongoPushEnabled = mongoConnected;
927
1442
 
928
- /** Chemins workspace : payload, puis cfg, puis instantané LOC. */
929
- const resolvedWorkspaceRoots = useMemo(() => {
930
- const top = workspaceFolderPathStrings(payload);
931
- if (top.length > 0) {
932
- return top;
933
- }
934
- const cfgObj = payload?.cfg as Record<string, unknown> | undefined;
935
- const fromCfg = workspaceFolderPathStrings(cfgObj);
936
- if (fromCfg.length > 0) {
937
- return fromCfg;
938
- }
939
- const snap = payload?.workspaceCodeSnapshot as
940
- | WorkspaceCodeSnapshotPayload
941
- | undefined;
942
- if (snap?.ok === true) {
943
- const w = snap.workspaceFolder?.trim();
944
- if (w) {
945
- return [w];
946
- }
947
- }
948
- return [];
949
- }, [payload]);
950
-
951
1443
  const gitStats = payload?.gitStats as GitRepoStatisticsPayload | undefined;
952
1444
 
953
- const showWorkspaceFoldersEmpty = showWorkspaceFoldersEmptyMessage(
954
- payload,
955
- resolvedWorkspaceRoots.length,
956
- );
957
1445
  const showIdeCodeTimingMetrics = showIdeLinkedCodeTimingMetrics(payload, cfg);
958
1446
 
959
- const showHeaderUserRow =
960
- Boolean(payload) &&
961
- Boolean(
962
- gitContextLine ||
963
- showWorkspaceFoldersEmpty ||
964
- resolvedWorkspaceRoots.length > 0 ||
965
- cfg,
966
- );
967
- const headerClockShort = dashboardUse24HourClock
968
- ? dt.headerClockFormat24Short
969
- : dt.headerClockFormat12Short;
970
- const headerDisplayPrefsTitle = dt.headerDisplayRegionTitle
971
- .replace("{timeZone}", dashboardDisplayTimeZone)
972
- .replace("{clock}", headerClockShort);
1447
+ const renderSelectedSessionSidebarCard = () => (
1448
+ <SelectedSessionSidebarBlock
1449
+ lang={lang}
1450
+ t={dt}
1451
+ sessionCurrent={sessionCurrent}
1452
+ columnArchiveId={columnArchiveId}
1453
+ sessionName={sessionName}
1454
+ setSessionName={setSessionName}
1455
+ sessionNameInputRef={sessionNameInputRef}
1456
+ sessionNameFieldActiveRef={sessionNameFieldActiveRef}
1457
+ sessionNameRowFocused={sessionNameRowFocused}
1458
+ setSessionNameRowFocused={setSessionNameRowFocused}
1459
+ post={post}
1460
+ headerSessionLabel={header.session}
1461
+ headerSessionDuration={header.sessionDuration}
1462
+ headerSessionStart={header.sessionStart}
1463
+ displayTimeZone={dashboardDisplayTimeZone}
1464
+ use24HourClock={dashboardUse24HourClock}
1465
+ headerCoding={header.coding}
1466
+ headerActive={header.active}
1467
+ headerTasks={header.tasks}
1468
+ archivedBadge={dt.archivedBadge}
1469
+ trackCodeMetrics={trackCodeMetrics}
1470
+ showIdeCodeTimingMetrics={showIdeCodeTimingMetrics}
1471
+ gitStats={gitStats}
1472
+ liveSessionId={live?.sessionId}
1473
+ onEndLiveSession={() => void handleRequestEndLiveSession()}
1474
+ sessionDurationAlertThresholdMinutes={
1475
+ sessionDurationAlertThresholdMinutes
1476
+ }
1477
+ allowSessionStartTimeEdit={dashboardAllowSessionStartTimeEdit}
1478
+ allowSessionEndTimeEdit={dashboardAllowSessionEndTimeEdit}
1479
+ onOpenTaskGantt={() => {
1480
+ const sid =
1481
+ typeof sessionCurrent?.sessionId === "string"
1482
+ ? sessionCurrent.sessionId.trim()
1483
+ : "";
1484
+ if (sid) {
1485
+ openGanttForSessionId(sid);
1486
+ }
1487
+ }}
1488
+ />
1489
+ );
973
1490
 
974
1491
  return (
975
1492
  <div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
976
1493
  <header className={appShellHeaderClassName}>
977
- <div className="mb-3 flex w-full flex-col gap-3 sm:mb-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
1494
+ <div className={appShellHeaderTitleMetaRowClassName}>
978
1495
  <div id="dashboard-tour-anchor-intro" className="min-w-0 shrink-0">
979
1496
  <div className="flex min-w-0 flex-col gap-1">
980
1497
  <h1 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
@@ -992,79 +1509,11 @@ function DashboardHome() {
992
1509
  </p>
993
1510
  </div>
994
1511
  </div>
995
- <div
996
- id="dashboard-tour-anchor-user-storage"
997
- className="min-w-0 shrink-0 sm:max-w-[min(100%,36rem)] sm:justify-self-end"
998
- >
999
- {showHeaderUserRow ? (
1000
- <div className="flex min-w-0 flex-col items-stretch gap-1.5 text-xs text-zinc-600 sm:items-end sm:text-right dark:text-zinc-400">
1001
- {gitContextLine ? (
1002
- <p
1003
- className="flex max-w-full items-center gap-x-2 sm:justify-end"
1004
- title={gitContextLine}
1005
- >
1006
- <User
1007
- className="shrink-0 text-zinc-500 dark:text-zinc-500"
1008
- size={14}
1009
- aria-hidden
1010
- />
1011
- <span className="min-w-0 max-w-[min(100%,48rem)] truncate font-medium text-zinc-800 dark:text-zinc-300">
1012
- {gitContextLine}
1013
- </span>
1014
- </p>
1015
- ) : null}
1016
- <p className="flex max-w-full flex-wrap items-center gap-x-1.5 sm:justify-end" title={headerDisplayPrefsTitle}>
1017
- <Globe className="shrink-0 text-zinc-500 dark:text-zinc-500" size={14} aria-hidden />
1018
- <span className="min-w-0 max-w-[min(100%,48rem)] break-all font-mono text-[0.7rem] font-medium text-zinc-800 dark:text-zinc-300">
1019
- {dashboardDisplayTimeZone}
1020
- </span>
1021
- <span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
1022
- ·
1023
- </span>
1024
- <Clock className="shrink-0 text-zinc-500 dark:text-zinc-500" size={14} aria-hidden />
1025
- <span className="shrink-0 font-medium text-zinc-800 dark:text-zinc-300">{headerClockShort}</span>
1026
- </p>
1027
- {showWorkspaceFoldersEmpty ? (
1028
- <p className="flex max-w-full items-start gap-x-2 sm:justify-end">
1029
- <FolderOpen
1030
- className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-500"
1031
- size={14}
1032
- aria-hidden
1033
- />
1034
- <span className="min-w-0 max-w-[min(100%,48rem)] break-words font-medium text-zinc-800 sm:text-right dark:text-zinc-300">
1035
- {dt.workspaceFoldersEmpty ?? "—"}
1036
- </span>
1037
- </p>
1038
- ) : resolvedWorkspaceRoots.length > 0 ? (
1039
- resolvedWorkspaceRoots.map((p) => (
1040
- <p
1041
- key={p}
1042
- className="flex max-w-full items-start gap-x-2 sm:justify-end"
1043
- >
1044
- <FolderOpen
1045
- className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-500"
1046
- size={14}
1047
- aria-hidden
1048
- />
1049
- <span className="min-w-0 max-w-[min(100%,48rem)] break-all font-medium text-zinc-800 sm:text-right dark:text-zinc-300">
1050
- {p}
1051
- </span>
1052
- </p>
1053
- ))
1054
- ) : null}
1055
- {cfg ? (
1056
- <div className="flex w-full min-w-0 sm:justify-end">
1057
- <HeaderIntegrationBadges
1058
- t={dt}
1059
- localPersistenceDriver={localPersistenceDriver}
1060
- mongoConnected={mongoConnected}
1061
- mongoEnabled={mongoEnabled}
1062
- />
1063
- </div>
1064
- ) : null}
1065
- </div>
1066
- ) : null}
1067
- </div>
1512
+ <AppShellHeaderSessionMeta
1513
+ domId="dashboard-tour-anchor-user-storage"
1514
+ payload={payload}
1515
+ dt={dt}
1516
+ />
1068
1517
  </div>
1069
1518
  <div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] xl:items-center xl:gap-x-6 xl:gap-y-0">
1070
1519
  <div className="flex flex-wrap items-center justify-between gap-4 xl:contents">
@@ -1074,8 +1523,9 @@ function DashboardHome() {
1074
1523
  />
1075
1524
  <div
1076
1525
  id="dashboard-tour-anchor-app-toolbar"
1077
- className="flex h-10 shrink-0 flex-wrap items-center justify-end gap-1.5 xl:col-start-3 xl:row-start-1 xl:justify-self-end"
1526
+ className={`${appShellHeaderToolbarClassName} xl:col-start-3 xl:row-start-1 xl:justify-self-end`}
1078
1527
  >
1528
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
1079
1529
  <DashboardCommandCenter
1080
1530
  dt={dt}
1081
1531
  handlers={commandHandlers}
@@ -1087,6 +1537,25 @@ function DashboardHome() {
1087
1537
  labels={nav}
1088
1538
  navAriaLabel={dt.appShellRouteNavAria}
1089
1539
  dashboardSessionId={selectedSessionId}
1540
+ ganttControl={{
1541
+ label: dt.appShellRouteNavTodayGantt,
1542
+ onPress: openTodayGanttModal,
1543
+ }}
1544
+ reserveGlobalPauseSlot={!(live && live.archived !== true)}
1545
+ globalPauseControl={
1546
+ live && live.archived !== true
1547
+ ? {
1548
+ active: globalPauseContextActive,
1549
+ label: globalPauseContextActive
1550
+ ? dt.appShellRouteNavGlobalResume
1551
+ : dt.appShellRouteNavGlobalPause,
1552
+ disabled: globalPauseNavDisabled,
1553
+ disabledTooltip:
1554
+ dt.appShellRouteNavGlobalPauseDisabledTooltip,
1555
+ onPress: onGlobalPauseNavPress,
1556
+ }
1557
+ : undefined
1558
+ }
1090
1559
  />
1091
1560
  <ThemeToggle lang={lang} />
1092
1561
  <PageRefreshButton
@@ -1133,8 +1602,8 @@ function DashboardHome() {
1133
1602
  typeof id === "string" && id.length > 0,
1134
1603
  )
1135
1604
  : live.activeTask?.id
1136
- ? [live.activeTask.id]
1137
- : undefined
1605
+ ? [live.activeTask.id]
1606
+ : undefined
1138
1607
  }
1139
1608
  t={dt}
1140
1609
  post={postVoid}
@@ -1145,6 +1614,18 @@ function DashboardHome() {
1145
1614
  </div>
1146
1615
  </header>
1147
1616
 
1617
+ {payload &&
1618
+ live &&
1619
+ live.archived !== true &&
1620
+ typeof live.sessionId === "string" &&
1621
+ live.sessionId.trim() !== "" &&
1622
+ (live.isPaused === true || globalPauseContextActive) ? (
1623
+ <DashboardPauseBackdrop
1624
+ variant={globalPauseContextActive ? "global" : "session"}
1625
+ dt={dt}
1626
+ />
1627
+ ) : null}
1628
+
1148
1629
  <div className="mx-auto w-full max-w-[1920px] px-5 pt-5 pb-8 sm:px-8 sm:pt-6 sm:pb-10 lg:px-12 lg:pt-7 lg:pb-11 xl:px-14 xl:pt-7 xl:pb-12">
1149
1630
  {error && (
1150
1631
  <div
@@ -1242,10 +1723,10 @@ function DashboardHome() {
1242
1723
  <div className="grid w-full grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,2.25fr)_minmax(0,1fr)] xl:items-start xl:gap-x-6 2xl:gap-x-10">
1243
1724
  <div
1244
1725
  id="dashboard-col-sessions"
1245
- className="flex min-w-0 flex-col xl:sticky xl:top-8 xl:max-h-[calc(100vh-6rem)] xl:overflow-y-auto xl:pb-6 xl:pr-3 [scrollbar-gutter:stable] 2xl:pr-4"
1726
+ className="flex min-w-0 flex-col xl:min-h-0 xl:overflow-visible xl:pb-6 xl:pr-3 2xl:pr-4"
1246
1727
  >
1247
- <div className="w-full min-w-0">
1248
- <div className="mx-auto flex w-full max-w-md flex-col gap-8 sm:max-w-lg xl:mx-0 xl:max-w-none">
1728
+ <div className="w-full min-w-0 xl:flex xl:min-h-0 xl:flex-1 xl:flex-col">
1729
+ <div className="mx-auto flex w-full max-w-md flex-col gap-8 sm:max-w-lg xl:mx-0 xl:max-w-none xl:min-h-0 xl:flex-1 xl:flex-col">
1249
1730
  <div
1250
1731
  className={`min-w-0 ${dashboardColumnTitleRowClassName}`}
1251
1732
  >
@@ -1273,76 +1754,63 @@ function DashboardHome() {
1273
1754
  </button>
1274
1755
  </div>
1275
1756
 
1276
- <SelectedSessionSidebarBlock
1277
- lang={lang}
1278
- t={dt}
1279
- sessionCurrent={sessionCurrent}
1280
- columnArchiveId={columnArchiveId}
1281
- sessionName={sessionName}
1282
- setSessionName={setSessionName}
1283
- sessionNameInputRef={sessionNameInputRef}
1284
- sessionNameFieldActiveRef={sessionNameFieldActiveRef}
1285
- sessionNameRowFocused={sessionNameRowFocused}
1286
- setSessionNameRowFocused={setSessionNameRowFocused}
1287
- post={post}
1288
- headerSessionLabel={header.session}
1289
- headerSessionDuration={header.sessionDuration}
1290
- headerSessionStart={header.sessionStart}
1291
- displayTimeZone={dashboardDisplayTimeZone}
1292
- use24HourClock={dashboardUse24HourClock}
1293
- headerCoding={header.coding}
1294
- headerActive={header.active}
1295
- headerTasks={header.tasks}
1296
- archivedBadge={dt.archivedBadge}
1297
- trackCodeMetrics={trackCodeMetrics}
1298
- showIdeCodeTimingMetrics={showIdeCodeTimingMetrics}
1299
- gitStats={gitStats}
1300
- liveSessionId={live?.sessionId}
1301
- onEndLiveSession={() =>
1302
- void handleRequestEndLiveSession()
1303
- }
1304
- sessionDurationAlertThresholdMinutes={
1305
- sessionDurationAlertThresholdMinutes
1306
- }
1307
- allowSessionStartTimeEdit={
1308
- dashboardAllowSessionStartTimeEdit
1309
- }
1310
- allowSessionEndTimeEdit={dashboardAllowSessionEndTimeEdit}
1311
- />
1312
-
1313
- <SessionListPanel
1314
- sessions={history}
1315
- lang={lang}
1316
- displayTimeZone={dashboardDisplayTimeZone}
1317
- use24HourClock={dashboardUse24HourClock}
1318
- liveSessionId={live?.sessionId}
1319
- selectedSessionId={selectedSessionId}
1320
- t={dt}
1321
- onSelectSession={(id) => void handleSelectSession(id)}
1322
- onOpenSessionInNewTab={openSessionInNewTab}
1323
- onEndLiveSession={() =>
1324
- void handleRequestEndLiveSession()
1325
- }
1326
- onArchiveSession={confirmArchiveSession}
1327
- onDeleteSession={(id) => setDeleteSessionId(id)}
1328
- onOpenArchives={() =>
1329
- void router.push(
1330
- withDashboardSessionParam(
1331
- "/settings#settings-archived-sessions",
1332
- selectedSessionId,
1333
- ),
1334
- )
1335
- }
1336
- archivedCount={historyArchived.length}
1337
- mongoPushEnabled={mongoPushEnabled}
1338
- onPushSessionToMongo={(id) =>
1339
- void handlePushSessionToMongo(id)
1340
- }
1341
- pushingSessionId={mongoPushBusyId}
1342
- sessionDurationAlertThresholdMinutes={
1343
- sessionDurationAlertThresholdMinutes
1344
- }
1345
- />
1757
+ {showSessionSidebarAboveSessionList
1758
+ ? renderSelectedSessionSidebarCard()
1759
+ : null}
1760
+
1761
+ <div className="min-h-[14rem] xl:h-full xl:min-h-0 xl:flex-1">
1762
+ <SessionListPanel
1763
+ sessions={sessionListPanelSessions}
1764
+ lang={lang}
1765
+ displayTimeZone={dashboardDisplayTimeZone}
1766
+ use24HourClock={dashboardUse24HourClock}
1767
+ liveSessionId={live?.sessionId}
1768
+ selectedSessionId={selectedSessionId}
1769
+ t={dt}
1770
+ onSelectSession={(id) => void handleSelectSession(id)}
1771
+ onOpenSessionInNewTab={openSessionInNewTab}
1772
+ onEndLiveSession={() =>
1773
+ void handleRequestEndLiveSession()
1774
+ }
1775
+ onArchiveSession={confirmArchiveSession}
1776
+ onDeleteSession={(id) => setDeleteSessionId(id)}
1777
+ onOpenArchives={() =>
1778
+ void router.push(
1779
+ withDashboardSessionParam(
1780
+ "/settings#settings-archived-sessions",
1781
+ selectedSessionId,
1782
+ ),
1783
+ )
1784
+ }
1785
+ archivedCount={historyArchived.length}
1786
+ mongoPushEnabled={mongoPushEnabled}
1787
+ onPushSessionToMongo={(id) =>
1788
+ void handlePushSessionToMongo(id)
1789
+ }
1790
+ pushingSessionId={mongoPushBusyId}
1791
+ sessionDurationAlertThresholdMinutes={
1792
+ sessionDurationAlertThresholdMinutes
1793
+ }
1794
+ onOpenSessionGantt={openGanttForSessionId}
1795
+ sessionRowExitAnimateId={sessionRowExitAnimateId}
1796
+ onSessionRowExitAnimationDone={
1797
+ clearSessionRowExitStates
1798
+ }
1799
+ liveChromeExitSessionId={endLiveListExitAnimateId}
1800
+ sortPinSessionId={endLiveListExitAnimateId}
1801
+ sessionDetailInline={
1802
+ archiveSessionInMainHistoryList &&
1803
+ typeof columnArchiveId === "string" &&
1804
+ columnArchiveId.length > 0
1805
+ ? {
1806
+ sessionId: columnArchiveId,
1807
+ content: renderSelectedSessionSidebarCard(),
1808
+ }
1809
+ : null
1810
+ }
1811
+ forcePageScroll
1812
+ />
1813
+ </div>
1346
1814
  </div>
1347
1815
  </div>
1348
1816
  </div>
@@ -1376,12 +1844,13 @@ function DashboardHome() {
1376
1844
  showKronoFocusInTaskOps={dashboardShowKronoFocusInTaskOps}
1377
1845
  allowTaskStartTimeEdit={dashboardAllowTaskStartTimeEdit}
1378
1846
  allowTaskEndTimeEdit={dashboardAllowTaskEndTimeEdit}
1847
+ taskLauncherApplyDraftRef={taskLauncherApplyDraftRef}
1379
1848
  />
1380
1849
  </div>
1381
1850
 
1382
1851
  <aside
1383
1852
  id="dashboard-col-tags"
1384
- className="flex min-w-0 flex-col xl:sticky xl:top-8 xl:max-h-[calc(100vh-6rem)] xl:overflow-y-auto xl:pb-6 xl:pl-3 [scrollbar-gutter:stable] 2xl:pl-4"
1853
+ className="flex min-w-0 flex-col xl:sticky xl:top-44 xl:max-h-[calc(100vh-12rem)] xl:overflow-y-auto xl:pb-6 xl:pl-3 [scrollbar-gutter:stable] 2xl:pl-4"
1385
1854
  aria-labelledby="dashboard-tags-projects-heading"
1386
1855
  >
1387
1856
  <div className={`min-w-0 ${dashboardColumnTitleRowClassName}`}>
@@ -1425,11 +1894,61 @@ function DashboardHome() {
1425
1894
  lang={lang}
1426
1895
  t={dt}
1427
1896
  onCancel={() => setNewSessionModalOpen(false)}
1428
- onConfirm={(sessionScope) => {
1429
- setNewSessionModalOpen(false);
1430
- void post({ type: "newSession", sessionScope });
1897
+ onConfirm={(payload) => {
1898
+ const liveSid =
1899
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
1900
+ const hasActiveLive = liveSid !== "" && live?.archived !== true;
1901
+ if (!payload.sessionStartAtIso && hasActiveLive) {
1902
+ pendingNewSessionAfterEndRef.current = {
1903
+ sessionScope: payload.scope,
1904
+ sessionStartAtIso: null,
1905
+ sessionEndAtIso: null,
1906
+ };
1907
+ setNewSessionModalOpen(false);
1908
+ handleRequestEndLiveSession();
1909
+ return;
1910
+ }
1911
+ void createNewSessionAndFocus({
1912
+ sessionScope: payload.scope,
1913
+ sessionStartAtIso: payload.sessionStartAtIso,
1914
+ sessionEndAtIso: payload.sessionEndAtIso,
1915
+ });
1431
1916
  }}
1432
1917
  />
1918
+ <GlobalPauseConfirmModal
1919
+ open={globalPauseConfirmOpen}
1920
+ preview={globalPauseActivationPreview}
1921
+ lang={lang}
1922
+ displayTimeZone={dashboardDisplayTimeZone}
1923
+ use24HourClock={dashboardUse24HourClock}
1924
+ t={dt}
1925
+ onCancel={cancelGlobalPauseConfirm}
1926
+ onConfirm={confirmGlobalPauseActivation}
1927
+ />
1928
+ <TaskTimelineGanttModal
1929
+ open={ganttModalOpen}
1930
+ onClose={closeGanttModal}
1931
+ lang={lang}
1932
+ displayTimeZone={dashboardDisplayTimeZone}
1933
+ use24HourClock={dashboardUse24HourClock}
1934
+ t={dt}
1935
+ rows={ganttTimelineRows}
1936
+ sessionStartMs={ganttSessionStartMs}
1937
+ sessionEndMs={ganttSessionEndMs}
1938
+ />
1939
+ <TaskTimelineGanttModal
1940
+ open={todayGanttModalOpen}
1941
+ onClose={closeTodayGanttModal}
1942
+ lang={lang}
1943
+ displayTimeZone={dashboardDisplayTimeZone}
1944
+ use24HourClock={dashboardUse24HourClock}
1945
+ t={dt}
1946
+ title={dt.tasksTimelineGanttTodayTitle}
1947
+ description={dt.tasksTimelineGanttTodayDescription}
1948
+ rows={todayGanttTimelineRows}
1949
+ sessionStartMs={null}
1950
+ sessionEndMs={null}
1951
+ />
1433
1952
  <DashboardAlertModal
1434
1953
  open={homeDialogAlert !== null}
1435
1954
  message={homeDialogAlert ?? ""}
@@ -1509,6 +2028,38 @@ function DashboardHome() {
1509
2028
  </label>
1510
2029
  ))}
1511
2030
  </fieldset>
2031
+ {endLiveSessionConfirmFlags.hasPausedOrPendingTasks ||
2032
+ endLiveSessionConfirmFlags.hasActiveTracking ? (
2033
+ <fieldset className="space-y-2 border-0 p-0">
2034
+ <legend className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
2035
+ {dt.sessionEndLiveTasksHandlingLegend}
2036
+ </legend>
2037
+ {(
2038
+ [
2039
+ ["keep", dt.sessionEndLiveTasksHandlingKeep],
2040
+ ["finish", dt.sessionEndLiveTasksHandlingFinish],
2041
+ [
2042
+ "moveToPausedSession",
2043
+ dt.sessionEndLiveTasksHandlingMoveToPaused,
2044
+ ],
2045
+ ] as const
2046
+ ).map(([value, label]) => (
2047
+ <label
2048
+ key={value}
2049
+ className="flex cursor-pointer items-start gap-2 rounded-md py-0.5 pr-1 hover:bg-zinc-100/80 dark:hover:bg-zinc-800/50"
2050
+ >
2051
+ <input
2052
+ type="radio"
2053
+ name="kronosys-session-end-task-handling"
2054
+ className="mt-1 size-4 shrink-0 border-zinc-300 text-violet-600 focus:ring-violet-500/50 dark:border-zinc-600"
2055
+ checked={endSessionTaskHandling === value}
2056
+ onChange={() => setEndSessionTaskHandling(value)}
2057
+ />
2058
+ <span className="leading-snug">{label}</span>
2059
+ </label>
2060
+ ))}
2061
+ </fieldset>
2062
+ ) : null}
1512
2063
  <label className="block">
1513
2064
  <span className="sr-only">{dt.sessionEndReasonNoteAria}</span>
1514
2065
  <textarea
@@ -1545,6 +2096,10 @@ function DashboardHome() {
1545
2096
  setArchiveConfirmSessionId(null);
1546
2097
  setArchiveDismissChecked(false);
1547
2098
  if (id) {
2099
+ const snap = history.find((s) => s.sessionId === id);
2100
+ if (snap) {
2101
+ setArchiveListExitSession(snap);
2102
+ }
1548
2103
  await post({
1549
2104
  type: "archiveSession",
1550
2105
  sessionId: id,