@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
@@ -2,17 +2,20 @@
2
2
 
3
3
  import type { RefObject } from "react";
4
4
  import { useEffect, useMemo, useState } from "react";
5
- import { CheckCircle2, Pencil } from "lucide-react";
5
+ import { CheckCircle2, ChevronDown, LayoutGrid, Pencil } from "lucide-react";
6
6
  import { InlineMetricHelpTrigger } from "@/components/dashboard/InlineMetricHelpTrigger";
7
7
  import { SessionLocMetricsSection } from "@/components/dashboard/SessionLocMetricsSection";
8
8
  import { WorkspaceGitRepoCard } from "@/components/dashboard/WorkspaceGitRepoCard";
9
9
  import { formatDuration, formatWallDurationMs } from "@/lib/taskParsing";
10
- import { tbEmeraldIcon } from "@/lib/translucentButtonClasses";
10
+ import { tbEmeraldIcon, tbVioletIcon } from "@/lib/translucentButtonClasses";
11
11
  import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
12
12
  import type { GitRepoStatisticsPayload } from "@/lib/kronosysApi";
13
13
  import { formatSessionEndReasonLine } from "@/lib/sessionEndReason";
14
14
  import { SessionEndReasonEditor } from "@/components/dashboard/SessionEndReasonEditor";
15
- import { countSessionTasksForSidebar } from "@/lib/sessionTaskSidebarStats";
15
+ import {
16
+ countSessionTasksForSidebar,
17
+ getSessionTaskTimerBreakdown,
18
+ } from "@/lib/sessionTaskSidebarStats";
16
19
  import { useSmoothStopwatchDisplayMs } from "@/components/dashboard/useSmoothStopwatchMs";
17
20
  import {
18
21
  KronosysDatetimePopoverField,
@@ -27,31 +30,62 @@ type SessionMetricsShape = {
27
30
  archived?: boolean;
28
31
  /** Session en pause (collecte) : la durée murale ne progresse pas tant que la session est en pause. */
29
32
  isPaused?: boolean;
33
+ /** Contexte de pause globale sur la session live (présent tant que la reprise globale n’a pas eu lieu). */
34
+ globalPauseContext?: unknown;
30
35
  /** Horodatage immuable de création de la session ; repli : `startAt` pour les anciennes données. */
31
36
  createdAt?: string | null;
32
37
  /** Début officiel de la session (ISO 8601), aligné sur l’historique / la liste. */
33
38
  startAt?: string | null;
34
39
  endAt?: string | null;
40
+ scheduledEndAt?: string | null;
35
41
  sessionEndReasonKind?: string;
36
42
  sessionEndReasonNote?: string;
43
+ sessionNote?: string;
37
44
  sessionDurationMinutes?: number;
38
45
  codingMinutesSession?: number;
39
46
  activeMinutes?: number;
40
47
  totalEvents?: number;
41
48
  tasks?: Array<{
42
49
  id?: string;
50
+ name?: string;
51
+ startTime?: string;
52
+ endTime?: string;
53
+ durationMs?: number;
54
+ project?: string | null;
43
55
  isDone?: boolean;
44
56
  manualTaskTimerPaused?: boolean;
57
+ subtasks?: Array<{ id?: string; durationMs?: number }>;
58
+ activeSubtaskTimerId?: string | null;
59
+ subtaskTimerStartedAt?: string | number;
60
+ mainTimerSegmentStartedAt?: string | null;
45
61
  }>;
46
62
  activeTasks?: Array<{
47
63
  id?: string;
64
+ name?: string;
65
+ startTime?: string;
66
+ endTime?: string;
67
+ durationMs?: number;
68
+ project?: string | null;
48
69
  isDone?: boolean;
49
70
  manualTaskTimerPaused?: boolean;
71
+ subtasks?: Array<{ id?: string; durationMs?: number }>;
72
+ activeSubtaskTimerId?: string | null;
73
+ subtaskTimerStartedAt?: string | number;
74
+ mainTimerSegmentStartedAt?: string | null;
50
75
  }>;
51
76
  activeTask?: {
52
77
  id?: string;
78
+ name?: string;
79
+ startTime?: string;
80
+ endTime?: string;
81
+ durationMs?: number;
82
+ project?: string | null;
53
83
  isDone?: boolean;
54
84
  manualTaskTimerPaused?: boolean;
85
+ subtasks?: Array<{ id?: string; durationMs?: number }>;
86
+ activeSubtaskTimerId?: string | null;
87
+ subtaskTimerStartedAt?: string | number;
88
+ mainTimerSegmentStartedAt?: string | null;
55
89
  } | null;
56
90
  linesWrittenTotal?: number;
57
91
  linesWrittenHuman?: number;
@@ -89,6 +123,9 @@ export function SelectedSessionSidebarBlock({
89
123
  sessionDurationAlertThresholdMinutes,
90
124
  allowSessionStartTimeEdit = true,
91
125
  allowSessionEndTimeEdit = true,
126
+ onOpenTaskGantt,
127
+ /** Cadre plein largeur (défaut) ou contenu plat pour modale élargie (moins de hauteur, grille horizontale). */
128
+ surface = "card",
92
129
  }: {
93
130
  lang: Lang;
94
131
  t: DashboardStrings;
@@ -126,6 +163,9 @@ export function SelectedSessionSidebarBlock({
126
163
  allowSessionStartTimeEdit?: boolean;
127
164
  /** Option `dashboardAllowSessionEndTimeEdit` : correction de la fin de session (terminée). */
128
165
  allowSessionEndTimeEdit?: boolean;
166
+ /** Ouvre la modale Gantt pour la session affichée dans cette carte. */
167
+ onOpenTaskGantt: () => void;
168
+ surface?: "card" | "modalEmbed";
129
169
  }) {
130
170
  const liveSid =
131
171
  typeof sessionCurrent?.sessionId === "string"
@@ -146,13 +186,17 @@ export function SelectedSessionSidebarBlock({
146
186
  number | null
147
187
  >(null);
148
188
  const [sessionEndDraft, setSessionEndDraft] = useState("");
189
+ const [sessionNoteDraft, setSessionNoteDraft] = useState("");
190
+ const [sessionClosureExpanded, setSessionClosureExpanded] = useState(false);
191
+ const [blinkNowMs, setBlinkNowMs] = useState(() => Date.now());
192
+ const [statsNowMs, setStatsNowMs] = useState(() => Date.now());
149
193
 
150
194
  const sessionWallMinutes =
151
195
  optimisticSessionWallMinutes !== null
152
196
  ? optimisticSessionWallMinutes
153
197
  : derivedSessionWallMinutes !== null
154
- ? derivedSessionWallMinutes
155
- : (sessionCurrent?.sessionDurationMinutes ?? 0);
198
+ ? derivedSessionWallMinutes
199
+ : sessionCurrent?.sessionDurationMinutes ?? 0;
156
200
  const sessionEnded =
157
201
  typeof sessionCurrent?.endAt === "string" &&
158
202
  sessionCurrent.endAt.trim() !== "";
@@ -175,7 +219,11 @@ export function SelectedSessionSidebarBlock({
175
219
  const sessionWallDisplayMinutes = sessionWallDisplayMs / 60_000;
176
220
  const threshold = Math.max(1, sessionDurationAlertThresholdMinutes);
177
221
  const sessionDurationOverThreshold = sessionWallDisplayMinutes >= threshold;
178
- const taskCounts = countSessionTasksForSidebar(sessionCurrent);
222
+ const taskCounts = countSessionTasksForSidebar(sessionCurrent, statsNowMs);
223
+ const taskTimerBreakdown = getSessionTaskTimerBreakdown(
224
+ sessionCurrent,
225
+ statsNowMs,
226
+ );
179
227
  const endReasonLine = sessionCurrent
180
228
  ? formatSessionEndReasonLine(
181
229
  t,
@@ -203,6 +251,10 @@ export function SelectedSessionSidebarBlock({
203
251
  targetSessionEndReasonId !== "" &&
204
252
  !inspectingLiveRunning &&
205
253
  (sessionEnded || inspectingArchive);
254
+ const hasSessionClosureBox =
255
+ hasSessionContext &&
256
+ (canEditSessionEndReason ||
257
+ (showSessionEndReason && Boolean(endReasonLine)));
206
258
  const canEditSessionStartTime =
207
259
  allowSessionStartTimeEdit &&
208
260
  hasSessionContext &&
@@ -211,11 +263,8 @@ export function SelectedSessionSidebarBlock({
211
263
  const canEditSessionEndTime =
212
264
  allowSessionEndTimeEdit &&
213
265
  hasSessionContext &&
214
- sessionEnded &&
215
266
  typeof sessionCurrent?.startAt === "string" &&
216
- sessionCurrent.startAt.trim() !== "" &&
217
- typeof sessionCurrent?.endAt === "string" &&
218
- sessionCurrent.endAt.trim() !== "";
267
+ sessionCurrent.startAt.trim() !== "";
219
268
  const [sessionStartDraft, setSessionStartDraft] = useState("");
220
269
 
221
270
  useEffect(() => {
@@ -235,6 +284,8 @@ export function SelectedSessionSidebarBlock({
235
284
  const raw =
236
285
  typeof sessionCurrent?.endAt === "string"
237
286
  ? sessionCurrent.endAt.trim()
287
+ : typeof sessionCurrent?.scheduledEndAt === "string"
288
+ ? sessionCurrent.scheduledEndAt.trim()
238
289
  : "";
239
290
  const parsed = raw ? new Date(raw) : null;
240
291
  if (!parsed || Number.isNaN(parsed.getTime())) {
@@ -242,7 +293,19 @@ export function SelectedSessionSidebarBlock({
242
293
  return;
243
294
  }
244
295
  setSessionEndDraft(formatDatetimeLocalValue(parsed));
245
- }, [sessionCurrent?.sessionId, sessionCurrent?.endAt]);
296
+ }, [
297
+ sessionCurrent?.sessionId,
298
+ sessionCurrent?.endAt,
299
+ sessionCurrent?.scheduledEndAt,
300
+ ]);
301
+
302
+ useEffect(() => {
303
+ setSessionNoteDraft(
304
+ typeof sessionCurrent?.sessionNote === "string"
305
+ ? sessionCurrent.sessionNote
306
+ : "",
307
+ );
308
+ }, [sessionCurrent?.sessionId, sessionCurrent?.sessionNote]);
246
309
 
247
310
  useEffect(() => {
248
311
  setOptimisticSessionWallMinutes(null);
@@ -252,6 +315,11 @@ export function SelectedSessionSidebarBlock({
252
315
  sessionCurrent?.endAt,
253
316
  ]);
254
317
 
318
+ useEffect(() => {
319
+ // Par défaut: bloc de clôture replié à chaque changement de session.
320
+ setSessionClosureExpanded(false);
321
+ }, [sessionCurrent?.sessionId, columnArchiveId]);
322
+
255
323
  useEffect(() => {
256
324
  const startMs =
257
325
  typeof sessionCurrent?.startAt === "string"
@@ -276,6 +344,24 @@ export function SelectedSessionSidebarBlock({
276
344
  setDerivedSessionWallMinutes(null);
277
345
  }, [sessionCurrent?.startAt, sessionCurrent?.endAt, smoothSessionWall]);
278
346
 
347
+ useEffect(() => {
348
+ const hasScheduledEnd =
349
+ typeof sessionCurrent?.scheduledEndAt === "string" &&
350
+ sessionCurrent.scheduledEndAt.trim() !== "";
351
+ if (!hasScheduledEnd || sessionEnded) {
352
+ return;
353
+ }
354
+ const interval = globalThis.setInterval(() => {
355
+ setBlinkNowMs(Date.now());
356
+ }, 1000);
357
+ return () => globalThis.clearInterval(interval);
358
+ }, [sessionCurrent?.scheduledEndAt, sessionEnded, sessionCurrent?.sessionId]);
359
+
360
+ useEffect(() => {
361
+ const id = globalThis.setInterval(() => setStatsNowMs(Date.now()), 1000);
362
+ return () => globalThis.clearInterval(id);
363
+ }, []);
364
+
279
365
  const applySessionStartTimeEdit = () => {
280
366
  if (!canEditSessionStartTime) {
281
367
  return;
@@ -288,6 +374,13 @@ export function SelectedSessionSidebarBlock({
288
374
  if (startAt === sessionCurrent?.startAt) {
289
375
  return;
290
376
  }
377
+ const endWallMs =
378
+ typeof sessionCurrent?.endAt === "string"
379
+ ? Date.parse(sessionCurrent.endAt)
380
+ : Number.NaN;
381
+ if (Number.isFinite(endWallMs) && startMs > endWallMs) {
382
+ return;
383
+ }
291
384
  if (smoothSessionWall) {
292
385
  setOptimisticSessionWallMinutes(
293
386
  Math.max(0, (Date.now() - startMs) / 60000),
@@ -313,7 +406,7 @@ export function SelectedSessionSidebarBlock({
313
406
  return;
314
407
  }
315
408
  const endAt = new Date(endMs).toISOString();
316
- if (endAt === sessionCurrent.endAt) {
409
+ if (endAt === (sessionCurrent.endAt ?? sessionCurrent.scheduledEndAt)) {
317
410
  return;
318
411
  }
319
412
  setOptimisticSessionWallMinutes(Math.max(0, (endMs - startMs) / 60000));
@@ -325,42 +418,165 @@ export function SelectedSessionSidebarBlock({
325
418
  };
326
419
 
327
420
  const sessionStartFormatted = useMemo(() => {
328
- const raw = typeof sessionCurrent?.startAt === "string" ? sessionCurrent.startAt.trim() : "";
421
+ const raw =
422
+ typeof sessionCurrent?.startAt === "string"
423
+ ? sessionCurrent.startAt.trim()
424
+ : "";
329
425
  if (!raw) {
330
426
  return null;
331
427
  }
332
428
  return formatIsoInstantShort(raw, lang, displayTimeZone, use24HourClock);
333
429
  }, [sessionCurrent?.startAt, displayTimeZone, lang, use24HourClock]);
430
+ const sessionEndFormatted = useMemo(() => {
431
+ const raw =
432
+ typeof sessionCurrent?.endAt === "string"
433
+ ? sessionCurrent.endAt.trim()
434
+ : typeof sessionCurrent?.scheduledEndAt === "string"
435
+ ? sessionCurrent.scheduledEndAt.trim()
436
+ : "";
437
+ if (!raw) {
438
+ return null;
439
+ }
440
+ return formatIsoInstantShort(raw, lang, displayTimeZone, use24HourClock);
441
+ }, [
442
+ sessionCurrent?.endAt,
443
+ sessionCurrent?.scheduledEndAt,
444
+ displayTimeZone,
445
+ lang,
446
+ use24HourClock,
447
+ ]);
334
448
  const sessionCreatedFormatted = useMemo(() => {
335
449
  const raw =
336
- typeof sessionCurrent?.createdAt === "string" && sessionCurrent.createdAt.trim() !== ""
450
+ typeof sessionCurrent?.createdAt === "string" &&
451
+ sessionCurrent.createdAt.trim() !== ""
337
452
  ? sessionCurrent.createdAt.trim()
338
453
  : typeof sessionCurrent?.startAt === "string"
339
- ? sessionCurrent.startAt.trim()
340
- : "";
454
+ ? sessionCurrent.startAt.trim()
455
+ : "";
341
456
  if (!raw) {
342
457
  return null;
343
458
  }
344
459
  return formatIsoInstantShort(raw, lang, displayTimeZone, use24HourClock);
345
- }, [sessionCurrent?.createdAt, sessionCurrent?.startAt, displayTimeZone, lang, use24HourClock]);
460
+ }, [
461
+ sessionCurrent?.createdAt,
462
+ sessionCurrent?.startAt,
463
+ displayTimeZone,
464
+ lang,
465
+ use24HourClock,
466
+ ]);
346
467
  const sessionNameFallback =
347
468
  liveSid !== ""
348
469
  ? sessionCreatedFormatted
349
470
  ? `${liveSid.slice(0, 8)} · ${sessionCreatedFormatted}`
350
471
  : liveSid.slice(0, 8)
351
472
  : lang === "fr"
352
- ? "Nom de la session"
353
- : "Session name";
473
+ ? "Nom de la session"
474
+ : "Session name";
475
+ const scheduledSessionEndMs =
476
+ typeof sessionCurrent?.scheduledEndAt === "string" &&
477
+ sessionCurrent.scheduledEndAt.trim() !== ""
478
+ ? Date.parse(sessionCurrent.scheduledEndAt)
479
+ : Number.NaN;
480
+ const sessionRemainingMs = Number.isFinite(scheduledSessionEndMs)
481
+ ? scheduledSessionEndMs - blinkNowMs
482
+ : Number.NaN;
483
+ let sessionEndAlertClassName = "";
484
+ if (
485
+ !sessionEnded &&
486
+ Number.isFinite(sessionRemainingMs) &&
487
+ sessionRemainingMs > 0
488
+ ) {
489
+ if (sessionRemainingMs <= 5 * 60_000) {
490
+ sessionEndAlertClassName = "kronosys-end-time-alert-fast";
491
+ } else if (sessionRemainingMs <= 15 * 60_000) {
492
+ sessionEndAlertClassName = "kronosys-end-time-alert-slow";
493
+ }
494
+ }
495
+
496
+ /** Session live au premier plan : pas d’archive / pas d’onglet détaché sur une autre session. */
497
+ const isLiveSessionSidebar = hasSessionContext && columnArchiveId === null;
498
+
499
+ const globalPauseActive =
500
+ isLiveSessionSidebar &&
501
+ sessionCurrent &&
502
+ typeof sessionCurrent === "object" &&
503
+ "globalPauseContext" in sessionCurrent &&
504
+ (sessionCurrent as Record<string, unknown>).globalPauseContext != null;
505
+ const sessionWallPausedLive =
506
+ isLiveSessionSidebar &&
507
+ sessionCurrent?.archived !== true &&
508
+ sessionCurrent?.isPaused === true;
509
+
510
+ let sessionSidebarHeading = t.selectedSessionSidebarTitleIdle;
511
+ if (hasSessionContext) {
512
+ sessionSidebarHeading = isLiveSessionSidebar
513
+ ? t.selectedSessionSidebarTitleLive
514
+ : t.selectedSessionSidebarTitle;
515
+ }
516
+
517
+ const isModalEmbed = surface === "modalEmbed";
518
+ const timingStartLabel = lang === "fr" ? "Début" : "Start";
519
+ const timingDurationLabel = lang === "fr" ? "Durée" : "Duration";
520
+ const timingTrackedTotalLabel =
521
+ lang === "fr" ? "Total minuté (tâches)" : "Tracked total (tasks)";
522
+ const timingEndLabel = lang === "fr" ? "Fin" : "End";
523
+ const metricCellClass = isModalEmbed
524
+ ? "w-full min-w-0"
525
+ : "w-full max-w-[11rem] sm:max-w-none";
526
+ const tasksCellClass = isModalEmbed
527
+ ? "w-full min-w-0"
528
+ : "w-full max-w-[13rem] sm:max-w-none";
529
+ const metricsSectionClass = [
530
+ "grid rounded-lg border border-zinc-200 bg-zinc-50/80 p-3 dark:border-zinc-700/80 dark:bg-zinc-900/35",
531
+ isModalEmbed
532
+ ? showIdeCodeTimingMetrics
533
+ ? "grid-cols-2 gap-2 text-left md:grid-cols-3 md:gap-3"
534
+ : "grid-cols-2 gap-2 text-left"
535
+ : showIdeCodeTimingMetrics
536
+ ? "justify-items-center gap-3 text-center sm:grid-cols-3"
537
+ : "justify-items-center gap-3 text-center sm:grid-cols-2",
538
+ ].join(" ");
539
+
540
+ const rootSurfaceClass = isModalEmbed
541
+ ? "flex min-w-0 flex-col gap-3 rounded-none border-0 bg-transparent p-0 text-left shadow-none sm:gap-4 dark:bg-transparent"
542
+ : "flex min-w-0 flex-col gap-5 rounded-xl border border-zinc-200 bg-white/90 p-5 text-center shadow-sm sm:p-6 dark:border-zinc-800 dark:bg-zinc-800/40 dark:shadow-none";
354
543
 
355
544
  return (
356
- <div className="flex min-w-0 flex-col gap-5 rounded-xl border border-zinc-200 bg-white/90 p-5 text-center shadow-sm sm:p-6 dark:border-zinc-800 dark:bg-zinc-800/40 dark:shadow-none">
545
+ <div className={rootSurfaceClass}>
357
546
  <div>
358
- <h2 className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
359
- {t.selectedSessionSidebarTitle}
547
+ <h2
548
+ className={`${
549
+ isLiveSessionSidebar
550
+ ? "text-xs font-semibold uppercase tracking-wide text-emerald-600 dark:text-emerald-400"
551
+ : "text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400"
552
+ } ${
553
+ isModalEmbed ? "text-left" : ""
554
+ } flex flex-wrap items-center gap-x-2 gap-y-1`}
555
+ >
556
+ <span>{sessionSidebarHeading}</span>
557
+ {globalPauseActive ? (
558
+ <span className="rounded-md border border-amber-600/55 bg-amber-950/55 px-2 py-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-amber-100 shadow-sm dark:border-amber-500/45 dark:bg-amber-950/65 dark:text-amber-50">
559
+ {t.selectedSessionSidebarPausedBadgeGlobal}
560
+ </span>
561
+ ) : sessionWallPausedLive ? (
562
+ <span className="rounded-md border border-amber-600/55 bg-amber-950/55 px-2 py-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-amber-100 shadow-sm dark:border-amber-500/45 dark:bg-amber-950/65 dark:text-amber-50">
563
+ {t.selectedSessionSidebarPausedBadgeSession}
564
+ </span>
565
+ ) : null}
360
566
  </h2>
361
567
  {hasSessionContext ? (
362
- <div className="mt-2 flex min-w-0 flex-wrap items-center justify-center gap-x-2 gap-y-1">
363
- <div className="flex min-w-0 max-w-full flex-1 basis-48 items-center justify-center gap-1.5 sm:max-w-md">
568
+ <div
569
+ className={`mt-2 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 ${
570
+ isModalEmbed ? "justify-start" : "justify-center"
571
+ }`}
572
+ >
573
+ <div
574
+ className={`flex min-w-0 max-w-full flex-1 basis-48 gap-1.5 sm:max-w-md ${
575
+ isModalEmbed
576
+ ? "items-center justify-start"
577
+ : "items-center justify-center"
578
+ }`}
579
+ >
364
580
  <input
365
581
  ref={sessionNameInputRef}
366
582
  id="kronosys-session-name-sidebar"
@@ -425,51 +641,161 @@ export function SelectedSessionSidebarBlock({
425
641
  {t.selectedSessionIdleHint}
426
642
  </p>
427
643
  )}
428
- {hasSessionContext && sessionStartFormatted ? (
429
- <p className="mt-2 flex flex-wrap items-center justify-center gap-x-1.5 gap-y-0.5 text-center text-[0.7rem] leading-tight text-zinc-600 dark:text-zinc-400">
430
- <span className="font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
431
- {headerSessionStart}
432
- </span>
433
- <span className="font-mono font-medium text-zinc-800 dark:text-zinc-200">{sessionStartFormatted}</span>
434
- </p>
435
- ) : null}
436
- </div>
437
-
438
- {hasSessionContext ? (
439
- <section className="grid justify-items-center gap-3 rounded-lg border border-zinc-200 bg-zinc-50/80 p-3 text-center sm:grid-cols-2 dark:border-zinc-700/80 dark:bg-zinc-900/35">
440
- <div className="w-full max-w-[11rem] sm:max-w-none">
441
- <div className="flex min-h-5 items-center justify-center gap-0.5">
442
- <span className="text-[0.65rem] uppercase text-zinc-500 dark:text-zinc-500">
443
- {headerSessionDuration}
644
+ {hasSessionContext ? (
645
+ <section className="mt-3 space-y-2 rounded-lg border border-zinc-200 bg-zinc-50/90 p-3 text-left text-zinc-700 dark:border-zinc-700/80 dark:bg-zinc-900/40 dark:text-zinc-300">
646
+ <div className="flex min-w-0 items-center gap-2 text-sm">
647
+ <span className="w-20 shrink-0 font-medium text-zinc-500 dark:text-zinc-400">
648
+ {timingStartLabel}
649
+ </span>
650
+ {canEditSessionStartTime ? (
651
+ <div className="min-w-0 flex-1">
652
+ <KronosysDatetimePopoverField
653
+ value={sessionStartDraft}
654
+ onChange={setSessionStartDraft}
655
+ onBlur={applySessionStartTimeEdit}
656
+ aria-label={timingStartLabel}
657
+ lang={lang}
658
+ t={t}
659
+ />
660
+ </div>
661
+ ) : (
662
+ <span className="min-w-0 flex-1 font-mono font-medium text-zinc-800 dark:text-zinc-200">
663
+ {sessionStartFormatted ?? "—"}
664
+ </span>
665
+ )}
666
+ </div>
667
+ <div className="flex min-w-0 items-center gap-2 text-sm">
668
+ <span className="w-20 shrink-0 font-medium text-zinc-500 dark:text-zinc-400">
669
+ {timingEndLabel}
670
+ </span>
671
+ {canEditSessionEndTime ? (
672
+ <div className={`min-w-0 flex-1 ${sessionEndAlertClassName}`}>
673
+ <KronosysDatetimePopoverField
674
+ value={sessionEndDraft}
675
+ onChange={setSessionEndDraft}
676
+ onBlur={applySessionEndTimeEdit}
677
+ aria-label={timingEndLabel}
678
+ lang={lang}
679
+ defaultTimeMode="next-half-hour"
680
+ t={t}
681
+ />
682
+ </div>
683
+ ) : (
684
+ <span
685
+ className={`min-w-0 flex-1 font-mono font-medium text-zinc-800 dark:text-zinc-200 ${sessionEndAlertClassName}`}
686
+ >
687
+ {sessionEndFormatted ?? "—"}
688
+ </span>
689
+ )}
690
+ </div>
691
+ <div className="flex min-w-0 items-center gap-2 text-sm">
692
+ <span className="w-20 shrink-0 font-medium text-zinc-500 dark:text-zinc-400">
693
+ {timingDurationLabel}
694
+ </span>
695
+ <span
696
+ className={`min-w-0 flex-1 font-semibold tabular-nums ${
697
+ sessionDurationOverThreshold
698
+ ? "kronosys-session-duration-alert"
699
+ : "text-zinc-900 dark:text-zinc-100"
700
+ }`}
701
+ title={
702
+ sessionDurationOverThreshold
703
+ ? lang === "fr"
704
+ ? `Durée murale au seuil d’alerte ou au-delà (${Math.round(
705
+ threshold / 60,
706
+ )} h — Paramètres)`
707
+ : `Wall-clock duration at or past alert threshold (${Math.round(
708
+ threshold / 60,
709
+ )} h — Settings)`
710
+ : undefined
711
+ }
712
+ >
713
+ {smoothSessionWall
714
+ ? formatWallDurationMs(sessionWallDisplayMs)
715
+ : formatDuration(sessionWallDisplayMinutes)}
444
716
  </span>
445
717
  <InlineMetricHelpTrigger
446
718
  ariaLabel={t.statsMetricSessionDurationHelpAria}
447
719
  body={t.statsMetricSessionDurationHelpBody}
448
720
  />
449
721
  </div>
450
- <div
451
- className={`text-xl font-semibold tabular-nums ${
452
- sessionDurationOverThreshold
453
- ? "kronosys-session-duration-alert"
454
- : "text-zinc-900 dark:text-zinc-100"
455
- }`}
456
- title={
457
- sessionDurationOverThreshold
458
- ? lang === "fr"
459
- ? `Durée murale au seuil d’alerte ou au-delà (${Math.round(threshold / 60)} h — Paramètres)`
460
- : `Wall-clock duration at or past alert threshold (${Math.round(threshold / 60)} h — Settings)`
461
- : undefined
462
- }
463
- >
464
- {smoothSessionWall
465
- ? formatWallDurationMs(sessionWallDisplayMs)
466
- : formatDuration(sessionWallDisplayMinutes)}
722
+ <div className="flex min-w-0 items-start gap-2 text-sm">
723
+ <span className="w-20 shrink-0 font-medium text-zinc-500 dark:text-zinc-400">
724
+ {timingTrackedTotalLabel}
725
+ </span>
726
+ <div className="min-w-0 flex-1 space-y-0.5 font-medium tabular-nums text-zinc-800 dark:text-zinc-200">
727
+ <div>
728
+ {lang === "fr"
729
+ ? `${taskTimerBreakdown.taskCount} tâche${
730
+ taskTimerBreakdown.taskCount > 1 ? "s" : ""
731
+ } (${formatWallDurationMs(
732
+ taskTimerBreakdown.taskTimersTotalMs,
733
+ )})`
734
+ : `${taskTimerBreakdown.taskCount} task${
735
+ taskTimerBreakdown.taskCount > 1 ? "s" : ""
736
+ } (${formatWallDurationMs(
737
+ taskTimerBreakdown.taskTimersTotalMs,
738
+ )})`}
739
+ </div>
740
+ <div>
741
+ {lang === "fr"
742
+ ? `${taskTimerBreakdown.subtaskCount} sous-tâche${
743
+ taskTimerBreakdown.subtaskCount > 1 ? "s" : ""
744
+ } (${formatWallDurationMs(
745
+ taskTimerBreakdown.subtaskTimersTotalMs,
746
+ )})`
747
+ : `${taskTimerBreakdown.subtaskCount} subtask${
748
+ taskTimerBreakdown.subtaskCount > 1 ? "s" : ""
749
+ } (${formatWallDurationMs(
750
+ taskTimerBreakdown.subtaskTimersTotalMs,
751
+ )})`}
752
+ </div>
753
+ </div>
467
754
  </div>
468
- </div>
755
+ <div className="space-y-1 text-sm">
756
+ <label
757
+ htmlFor="kronosys-session-note"
758
+ className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500"
759
+ >
760
+ {t.sessionNoteLabel}
761
+ </label>
762
+ <textarea
763
+ id="kronosys-session-note"
764
+ className="w-full rounded-lg border border-zinc-300 bg-white/90 px-3 py-2 text-sm text-zinc-800 outline-none transition focus:border-violet-500 dark:border-zinc-700 dark:bg-zinc-900/70 dark:text-zinc-100"
765
+ placeholder={t.sessionNotePlaceholder}
766
+ value={sessionNoteDraft}
767
+ onChange={(e) => setSessionNoteDraft(e.target.value)}
768
+ onBlur={() => {
769
+ if (
770
+ sessionNoteDraft === (sessionCurrent?.sessionNote ?? "")
771
+ ) {
772
+ return;
773
+ }
774
+ void post({
775
+ type: "setSessionNote",
776
+ sessionId: targetSessionEndReasonId,
777
+ note: sessionNoteDraft,
778
+ });
779
+ }}
780
+ rows={3}
781
+ />
782
+ </div>
783
+ </section>
784
+ ) : null}
785
+ </div>
786
+
787
+ {hasSessionContext ? (
788
+ <section className={metricsSectionClass}>
469
789
  {showIdeCodeTimingMetrics ? (
470
790
  <>
471
- <div className="w-full max-w-[11rem] sm:max-w-none">
472
- <div className="flex min-h-5 items-center justify-center gap-0.5">
791
+ <div className={metricCellClass}>
792
+ <div
793
+ className={`flex min-h-5 gap-0.5 ${
794
+ isModalEmbed
795
+ ? "items-center justify-start"
796
+ : "items-center justify-center"
797
+ }`}
798
+ >
473
799
  <span className="text-[0.65rem] uppercase text-zinc-500 dark:text-zinc-500">
474
800
  {headerCoding}
475
801
  </span>
@@ -478,12 +804,22 @@ export function SelectedSessionSidebarBlock({
478
804
  body={t.statsMetricCodingTimeHelpBody}
479
805
  />
480
806
  </div>
481
- <div className="text-xl font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
807
+ <div
808
+ className={`text-xl font-semibold tabular-nums text-zinc-900 dark:text-zinc-100 ${
809
+ isModalEmbed ? "text-left" : ""
810
+ }`}
811
+ >
482
812
  {formatDuration(sessionCurrent?.codingMinutesSession ?? 0)}
483
813
  </div>
484
814
  </div>
485
- <div className="w-full max-w-[11rem] sm:max-w-none">
486
- <div className="flex min-h-5 items-center justify-center gap-0.5">
815
+ <div className={metricCellClass}>
816
+ <div
817
+ className={`flex min-h-5 gap-0.5 ${
818
+ isModalEmbed
819
+ ? "items-center justify-start"
820
+ : "items-center justify-center"
821
+ }`}
822
+ >
487
823
  <span className="text-[0.65rem] uppercase text-zinc-500 dark:text-zinc-500">
488
824
  {headerActive}
489
825
  </span>
@@ -492,14 +828,24 @@ export function SelectedSessionSidebarBlock({
492
828
  body={t.statsMetricActiveTimeHelpBody}
493
829
  />
494
830
  </div>
495
- <div className="text-xl font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
831
+ <div
832
+ className={`text-xl font-semibold tabular-nums text-zinc-900 dark:text-zinc-100 ${
833
+ isModalEmbed ? "text-left" : ""
834
+ }`}
835
+ >
496
836
  {formatDuration(sessionCurrent?.activeMinutes ?? 0)}
497
837
  </div>
498
838
  </div>
499
839
  </>
500
840
  ) : null}
501
- <div className="w-full max-w-[13rem] sm:max-w-none">
502
- <div className="flex min-h-5 items-center justify-center gap-0.5">
841
+ <div className={tasksCellClass}>
842
+ <div
843
+ className={`flex min-h-5 gap-0.5 ${
844
+ isModalEmbed
845
+ ? "items-center justify-start"
846
+ : "items-center justify-center"
847
+ }`}
848
+ >
503
849
  <span className="text-[0.65rem] uppercase text-zinc-500 dark:text-zinc-500">
504
850
  {headerTasks}
505
851
  </span>
@@ -509,7 +855,13 @@ export function SelectedSessionSidebarBlock({
509
855
  align="end"
510
856
  />
511
857
  </div>
512
- <div className="mx-auto mt-1 w-full max-w-[12.5rem] space-y-1 text-left text-[0.7rem] leading-tight text-zinc-800 dark:text-zinc-200">
858
+ <div
859
+ className={`mt-1 w-full space-y-1 text-[0.7rem] leading-tight text-zinc-800 dark:text-zinc-200 ${
860
+ isModalEmbed
861
+ ? "max-w-none text-left"
862
+ : "mx-auto max-w-[12.5rem] text-left"
863
+ }`}
864
+ >
513
865
  <div className="flex items-baseline justify-between gap-2">
514
866
  <span className="min-w-0 shrink text-zinc-500 dark:text-zinc-400">
515
867
  {t.statsTasksRowRunning}
@@ -518,6 +870,14 @@ export function SelectedSessionSidebarBlock({
518
870
  {taskCounts.running}
519
871
  </span>
520
872
  </div>
873
+ <div className="flex items-baseline justify-between gap-2">
874
+ <span className="min-w-0 shrink text-zinc-500 dark:text-zinc-400">
875
+ {t.statsTasksRowPlanned}
876
+ </span>
877
+ <span className="shrink-0 tabular-nums font-semibold text-zinc-900 dark:text-zinc-100">
878
+ {taskCounts.planned}
879
+ </span>
880
+ </div>
521
881
  <div className="flex items-baseline justify-between gap-2">
522
882
  <span className="min-w-0 shrink text-zinc-500 dark:text-zinc-400">
523
883
  {t.statsTasksRowPausedList}
@@ -539,90 +899,100 @@ export function SelectedSessionSidebarBlock({
539
899
  </section>
540
900
  ) : null}
541
901
 
542
- {canEditSessionStartTime ? (
902
+ {hasSessionClosureBox ? (
543
903
  <div className="rounded-lg border border-zinc-200 bg-zinc-50/90 px-3 py-2.5 text-left text-zinc-700 dark:border-zinc-700/80 dark:bg-zinc-900/40 dark:text-zinc-300">
544
- <div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
545
- {t.sessionStartTimeEditSectionTitle}
546
- </div>
547
- <div className="mt-2 flex min-w-0 flex-wrap items-center gap-2">
548
- <KronosysDatetimePopoverField
549
- value={sessionStartDraft}
550
- onChange={setSessionStartDraft}
551
- onBlur={applySessionStartTimeEdit}
552
- aria-label={t.sessionStartTimeEditSectionTitle}
553
- lang={lang}
554
- t={t}
555
- />
556
- </div>
557
- </div>
558
- ) : null}
559
-
560
- {canEditSessionEndTime ? (
561
- <div className="rounded-lg border border-zinc-200 bg-zinc-50/90 px-3 py-2.5 text-left text-zinc-700 dark:border-zinc-700/80 dark:bg-zinc-900/40 dark:text-zinc-300">
562
- <div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
563
- {t.sessionEndTimeEditSectionTitle}
564
- </div>
565
- <div className="mt-2 flex min-w-0 flex-wrap items-center gap-2">
566
- <KronosysDatetimePopoverField
567
- value={sessionEndDraft}
568
- onChange={setSessionEndDraft}
569
- onBlur={applySessionEndTimeEdit}
570
- aria-label={t.sessionEndTimeEditSectionTitle}
571
- lang={lang}
572
- t={t}
904
+ <button
905
+ type="button"
906
+ className="flex w-full items-center justify-between gap-2 rounded-md text-left hover:text-zinc-900 dark:hover:text-zinc-100"
907
+ aria-expanded={sessionClosureExpanded}
908
+ onClick={() => setSessionClosureExpanded((v) => !v)}
909
+ >
910
+ <div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
911
+ {t.selectedSessionEndReasonTitle}
912
+ </div>
913
+ <ChevronDown
914
+ size={16}
915
+ className={`shrink-0 text-zinc-500 transition-transform dark:text-zinc-400 ${
916
+ sessionClosureExpanded ? "rotate-180" : ""
917
+ }`}
918
+ aria-hidden
573
919
  />
574
- </div>
920
+ </button>
921
+ {sessionClosureExpanded ? (
922
+ canEditSessionEndReason ? (
923
+ <div className="mt-2">
924
+ <SessionEndReasonEditor
925
+ t={t}
926
+ radioGroupName="kronosys-session-end-reason-edit-sidebar"
927
+ sessionId={targetSessionEndReasonId}
928
+ initialKind={sessionCurrent?.sessionEndReasonKind}
929
+ initialNote={sessionCurrent?.sessionEndReasonNote}
930
+ post={post}
931
+ />
932
+ </div>
933
+ ) : showSessionEndReason && endReasonLine ? (
934
+ <p className="mt-2 whitespace-pre-wrap text-[0.75rem] leading-snug">
935
+ {endReasonLine}
936
+ </p>
937
+ ) : null
938
+ ) : null}
575
939
  </div>
576
940
  ) : null}
577
941
 
578
- {hasSessionContext && canEditSessionEndReason ? (
579
- <div className="rounded-lg border border-zinc-200 bg-zinc-50/90 px-3 py-2.5 text-left text-zinc-700 dark:border-zinc-700/80 dark:bg-zinc-900/40 dark:text-zinc-300">
580
- <div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
581
- {t.selectedSessionEndReasonTitle}
582
- </div>
583
- <div className="mt-2">
584
- <SessionEndReasonEditor
585
- t={t}
586
- radioGroupName="kronosys-session-end-reason-edit-sidebar"
587
- sessionId={targetSessionEndReasonId}
588
- initialKind={sessionCurrent?.sessionEndReasonKind}
589
- initialNote={sessionCurrent?.sessionEndReasonNote}
590
- post={post}
591
- />
592
- </div>
593
- </div>
594
- ) : hasSessionContext && showSessionEndReason && endReasonLine ? (
595
- <div className="rounded-lg border border-zinc-200 bg-zinc-50/90 px-3 py-2.5 text-left text-[0.75rem] leading-snug text-zinc-700 dark:border-zinc-700/80 dark:bg-zinc-900/40 dark:text-zinc-300">
596
- <div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
597
- {t.selectedSessionEndReasonTitle}
942
+ {isModalEmbed && trackCodeMetrics ? (
943
+ <div className="grid gap-3 md:grid-cols-2 md:items-start">
944
+ {hasSessionContext ? (
945
+ <div className="min-w-0 text-left">
946
+ <SessionLocMetricsSection session={sessionCurrent ?? {}} t={t} />
947
+ </div>
948
+ ) : null}
949
+ <div className="min-w-0 text-left">
950
+ <WorkspaceGitRepoCard git={gitStats} t={t} lang={lang} />
598
951
  </div>
599
- <p className="mt-1 whitespace-pre-wrap">{endReasonLine}</p>
600
- </div>
601
- ) : null}
602
-
603
- {hasSessionContext && trackCodeMetrics ? (
604
- <div className="text-left">
605
- <SessionLocMetricsSection session={sessionCurrent ?? {}} t={t} />
606
- </div>
607
- ) : null}
608
-
609
- {trackCodeMetrics ? (
610
- <div className="text-left">
611
- <WorkspaceGitRepoCard git={gitStats} t={t} lang={lang} />
612
952
  </div>
953
+ ) : !isModalEmbed ? (
954
+ <>
955
+ {hasSessionContext && trackCodeMetrics ? (
956
+ <div className="text-left">
957
+ <SessionLocMetricsSection session={sessionCurrent ?? {}} t={t} />
958
+ </div>
959
+ ) : null}
960
+ {trackCodeMetrics ? (
961
+ <div className="text-left">
962
+ <WorkspaceGitRepoCard git={gitStats} t={t} lang={lang} />
963
+ </div>
964
+ ) : null}
965
+ </>
613
966
  ) : null}
614
967
 
615
- {showEndLiveSession ? (
616
- <div className="flex justify-end">
617
- <button
618
- type="button"
619
- className={tbEmeraldIcon}
620
- aria-label={t.sessionEndLiveAria}
621
- title={`${t.sessionEndLiveSidebarBtn} ${t.sessionEndLiveTitle}`}
622
- onClick={() => onEndLiveSession?.()}
623
- >
624
- <CheckCircle2 size={20} aria-hidden />
625
- </button>
968
+ {hasSessionContext || showEndLiveSession ? (
969
+ <div
970
+ className={`flex gap-2 ${
971
+ isModalEmbed ? "justify-start" : "justify-end"
972
+ }`}
973
+ >
974
+ {hasSessionContext ? (
975
+ <button
976
+ type="button"
977
+ className={tbVioletIcon}
978
+ aria-label={t.selectedSessionSidebarTimelineIconAria}
979
+ title={t.selectedSessionSidebarTimelineIconTooltip}
980
+ onClick={() => onOpenTaskGantt()}
981
+ >
982
+ <LayoutGrid size={20} aria-hidden />
983
+ </button>
984
+ ) : null}
985
+ {showEndLiveSession ? (
986
+ <button
987
+ type="button"
988
+ className={tbEmeraldIcon}
989
+ aria-label={t.sessionEndLiveAria}
990
+ title={`${t.sessionEndLiveSidebarBtn} — ${t.sessionEndLiveTitle}`}
991
+ onClick={() => onEndLiveSession?.()}
992
+ >
993
+ <CheckCircle2 size={20} aria-hidden />
994
+ </button>
995
+ ) : null}
626
996
  </div>
627
997
  ) : null}
628
998
  </div>