reflet-sdk 0.7.7 → 0.7.8

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.
@@ -1,5 +1,5 @@
1
- import { a as EDITORS, n as DEVTOOLS_REQUEST_HEADER, r as DEVTOOLS_ROUTE_BASE, t as DEVTOOLS_ENDPOINTS } from "./protocol-BnurX0CF.js";
2
- import { $ as inspectReactElement, A as PencilIcon, B as DEFAULT_WIDGET_LABELS, C as CheckIcon, D as GripIcon, E as CodeIcon, G as buildElementSelection, H as clip, J as getElementRect, K as buildSelector, N as Svg, P as TargetIcon, Q as getFiberFromNode, T as CloseIcon, U as collectPageContext, V as SDK_VERSION, Y as REDACT_ATTRIBUTE, Z as redactUrl, _ as isSubmitEnter, a as uploadBlob, at as capturedViewportSize, c as useFloatingPosition, d as SelectionOutline, et as resolveComponentStack, h as startsInWidget, it as capturedFromCanvas, k as MoreIcon, l as onViewportChange, m as listenToKeydown, o as ElementPicker, ot as releaseCapture, p as listenOnPageAndWidget, q as describeElement, r as deriveTitle, rt as captureViewport, s as isWidgetOwned, tt as shortenSourcePath, u as visibleViewport, v as matchesHotkey } from "./submit-r3_M1ITo.js";
1
+ import { a as EDITORS, n as DEVTOOLS_REQUEST_HEADER, r as DEVTOOLS_ROUTE_BASE, t as DEVTOOLS_ENDPOINTS } from "./protocol-ClxYWmhu.js";
2
+ import { $ as inspectReactElement, A as PencilIcon, B as DEFAULT_WIDGET_LABELS, C as CheckIcon, D as GripIcon, E as CodeIcon, G as buildElementSelection, H as clip, I as TrashIcon, J as getElementRect, K as buildSelector, N as Svg, P as TargetIcon, Q as getFiberFromNode, T as CloseIcon, U as collectPageContext, V as SDK_VERSION, Y as REDACT_ATTRIBUTE, Z as redactUrl, _ as isSubmitEnter, a as uploadBlob, at as capturedViewportSize, c as useFloatingPosition, d as SelectionOutline, et as resolveComponentStack, h as startsInWidget, it as capturedFromCanvas, k as MoreIcon, l as onViewportChange, m as listenToKeydown, o as ElementPicker, ot as releaseCapture, p as listenOnPageAndWidget, q as describeElement, r as deriveTitle, rt as captureViewport, s as isWidgetOwned, tt as shortenSourcePath, u as visibleViewport, v as matchesHotkey } from "./submit-D_DrRBeB.js";
3
3
  import { t as REFLET_Z_INDEX } from "./z-index-CwokqVq_.js";
4
4
  import { z } from "zod";
5
5
  import * as G$1 from "react";
@@ -630,7 +630,6 @@ function createProposalEndpoints(requests) {
630
630
  files: z.array(handoffFileSchema),
631
631
  handoff: sessionHandoffSchema
632
632
  })),
633
- createProposal: (body) => postProposal("/api/v1/sessions/proposals/create", body),
634
633
  decideProposal: (body) => postProposal("/api/v1/sessions/proposals/decide", body),
635
634
  editProposal: (body) => postProposal("/api/v1/sessions/proposals/edit", body),
636
635
  markHandoffDelivered: async (body) => (await post("/api/v1/sessions/handoffs/delivered", body, z.object({ handoff: sessionHandoffSchema }))).handoff,
@@ -920,6 +919,21 @@ function operationMethods(database) {
920
919
  id: request.result
921
920
  };
922
921
  },
922
+ cancelDraft: async (deletion) => {
923
+ const row = await packOperation(deletion);
924
+ await database.write([DRAFTS, OPERATIONS], (transaction) => {
925
+ const drafts = transaction.objectStore(DRAFTS);
926
+ const request = drafts.get(deletion.sessionKey);
927
+ request.onsuccess = () => {
928
+ const current = request.result;
929
+ if (current) drafts.put({
930
+ ...current,
931
+ state: "cancelled"
932
+ });
933
+ };
934
+ transaction.objectStore(OPERATIONS).add(row);
935
+ });
936
+ },
923
937
  deleteOperations: (ids) => database.write([OPERATIONS], (transaction) => {
924
938
  for (const id of ids) transaction.objectStore(OPERATIONS).delete(id);
925
939
  }),
@@ -1231,6 +1245,10 @@ async function sendOperation(context) {
1231
1245
  kind: "started",
1232
1246
  sessionId: (await context.api.start(payload.body)).id
1233
1247
  };
1248
+ if (payload.kind === "delete") {
1249
+ if (context.sessionId !== null) await context.api.deleteSession(context.sessionId);
1250
+ return { kind: "done" };
1251
+ }
1234
1252
  if (context.sessionId === null) return {
1235
1253
  kind: "drop",
1236
1254
  message: "This session was never created on Reflet."
@@ -1297,6 +1315,17 @@ var SessionOutbox = class {
1297
1315
  }
1298
1316
  });
1299
1317
  };
1318
+ async cancelSession(sessionKey) {
1319
+ const payload = { kind: "delete" };
1320
+ await this.options.store.cancelDraft({
1321
+ accountKey: this.options.accountKey,
1322
+ bytes: operationBytes(payload),
1323
+ payload,
1324
+ sessionKey
1325
+ });
1326
+ this.options.onDraftsChanged();
1327
+ this.drain();
1328
+ }
1300
1329
  async enqueue(sessionKey, payload) {
1301
1330
  const bytes = operationBytes(payload);
1302
1331
  if (this.snapshot.pendingBytes + bytes > this.options.maxLocalBufferBytes) this.failStorage(BUFFER_FULL);
@@ -1357,11 +1386,12 @@ var SessionOutbox = class {
1357
1386
  }
1358
1387
  async drainPass() {
1359
1388
  const { accountKey, store } = this.options;
1389
+ const drafts = await store.readDrafts(accountKey);
1360
1390
  const pass = {
1361
1391
  outcomes: /* @__PURE__ */ new Map(),
1362
- sessionIds: sessionIdsOf(await store.readDrafts(accountKey))
1392
+ sessionIds: sessionIdsOf(drafts)
1363
1393
  };
1364
- for (const operation of await store.readOperations(accountKey)) {
1394
+ for (const operation of await this.dropAbandonedUploads(drafts)) {
1365
1395
  if (this.isHalted()) return;
1366
1396
  if (pass.outcomes.get(operation.sessionKey)?.kind !== "retry") await this.runOperation(operation, pass);
1367
1397
  }
@@ -1373,6 +1403,16 @@ var SessionOutbox = class {
1373
1403
  await this.forgetFinishedDrafts(remaining);
1374
1404
  this.afterPass(pass);
1375
1405
  }
1406
+ async dropAbandonedUploads(drafts) {
1407
+ const { accountKey, store } = this.options;
1408
+ const cancelledSessionKeys = new Set(drafts.filter((draft) => draft.state === "cancelled").map((draft) => draft.sessionKey));
1409
+ const abandonedIds = [];
1410
+ const toSend = [];
1411
+ for (const operation of await store.readOperations(accountKey)) if (cancelledSessionKeys.has(operation.sessionKey) && operation.payload.kind !== "delete") abandonedIds.push(operation.id);
1412
+ else toSend.push(operation);
1413
+ await store.deleteOperations(abandonedIds);
1414
+ return toSend;
1415
+ }
1376
1416
  afterPass(pass) {
1377
1417
  if (![...pass.outcomes.values()].some((outcome) => outcome.kind === "retry")) {
1378
1418
  this.failedPasses = 0;
@@ -1442,7 +1482,7 @@ var SessionOutbox = class {
1442
1482
  async forgetFinishedDrafts(operations) {
1443
1483
  const { accountKey, store } = this.options;
1444
1484
  const waiting = new Set(operations.map((operation) => operation.sessionKey));
1445
- const finished = (await store.readDrafts(accountKey)).filter((draft) => draft.state === "ended" && !waiting.has(draft.sessionKey));
1485
+ const finished = (await store.readDrafts(accountKey)).filter((draft) => (draft.state === "ended" || draft.state === "cancelled") && !waiting.has(draft.sessionKey));
1446
1486
  await Promise.all(finished.map((draft) => store.deleteDraft(draft.sessionKey)));
1447
1487
  if (finished.length > 0) this.options.onDraftsChanged();
1448
1488
  }
@@ -1463,15 +1503,16 @@ const IDLE_SYNC = {
1463
1503
  pendingOperations: 0
1464
1504
  };
1465
1505
  function visibleDraft(stored, isLive) {
1506
+ if (stored.state === "cancelled") return [];
1466
1507
  const leftOpen = stored.state === "recording" || stored.state === "paused";
1467
- return {
1508
+ return [{
1468
1509
  lastActiveAt: stored.lastActiveAt,
1469
1510
  sessionId: stored.sessionId,
1470
1511
  sessionKey: stored.sessionKey,
1471
1512
  startedAt: stored.startedAt,
1472
1513
  state: leftOpen && !isLive ? "interrupted" : stored.state,
1473
1514
  title: stored.title
1474
- };
1515
+ }];
1475
1516
  }
1476
1517
  var LocalSessionsRegistry = class {
1477
1518
  accounts = /* @__PURE__ */ new Map();
@@ -1508,7 +1549,8 @@ var LocalSessionsRegistry = class {
1508
1549
  async discardDraft(options) {
1509
1550
  const { accountKey, sessionKey } = options;
1510
1551
  const stored = (await this.store.readDrafts(accountKey)).find((candidate) => candidate.sessionKey === sessionKey);
1511
- if (!stored || this.live.has(sessionKey) || stored.state === "ended") return;
1552
+ const isClosed = stored?.state === "ended" || stored?.state === "cancelled";
1553
+ if (!stored || this.live.has(sessionKey) || isClosed) return;
1512
1554
  const { outbox } = this.ensureOutbox({
1513
1555
  ...options,
1514
1556
  maxLocalBufferBytes: null
@@ -1544,7 +1586,7 @@ var LocalSessionsRegistry = class {
1544
1586
  }
1545
1587
  refreshDrafts(accountKey) {
1546
1588
  this.store.readDrafts(accountKey).then((stored) => {
1547
- const visible = stored.map((item) => visibleDraft(item, this.live.has(item.sessionKey))).sort((a, b) => b.startedAt - a.startedAt);
1589
+ const visible = stored.flatMap((item) => visibleDraft(item, this.live.has(item.sessionKey))).sort((a, b) => b.startedAt - a.startedAt);
1548
1590
  this.drafts.set(accountKey, visible);
1549
1591
  this.notify();
1550
1592
  }).catch(() => {
@@ -2866,6 +2908,13 @@ var RecordingController = class {
2866
2908
  annotateMark(key, note) {
2867
2909
  this.live?.annotate(key, cleanText(note, MAX_NOTE_CHARS));
2868
2910
  }
2911
+ async cancel() {
2912
+ const live = this.live;
2913
+ if (!live) return;
2914
+ const cancelling = this.account.outbox.cancelSession(live.sessionKey);
2915
+ this.close(live, IDLE);
2916
+ await cancelling;
2917
+ }
2869
2918
  dispose() {
2870
2919
  this.interrupt();
2871
2920
  this.stopIssues();
@@ -2921,7 +2970,7 @@ var RecordingController = class {
2921
2970
  });
2922
2971
  }
2923
2972
  async resumeDraft(sessionKey) {
2924
- const draft = (await this.account.store.readDrafts(this.options.account.key)).find((candidate) => candidate.sessionKey === sessionKey && candidate.state !== "ended");
2973
+ const draft = (await this.account.store.readDrafts(this.options.account.key)).find((candidate) => candidate.sessionKey === sessionKey && candidate.state !== "ended" && candidate.state !== "cancelled");
2925
2974
  if (!draft || this.isBusy()) return;
2926
2975
  const voice = await this.requestVoice();
2927
2976
  if (!voice) return;
@@ -3349,6 +3398,188 @@ function isDetailProcessing(detail) {
3349
3398
  return transcribing || refining || comparing || isAnalysisBusy(detail.session) || detail.session.collection !== "ended";
3350
3399
  }
3351
3400
  //#endregion
3401
+ //#region src/sessions/ui/review/use-session-detail.ts
3402
+ const POLL_INTERVAL_MS = 2e3;
3403
+ function nextRequest(current) {
3404
+ return {
3405
+ ...current,
3406
+ count: current.count + 1
3407
+ };
3408
+ }
3409
+ function withProposal(detail, proposal) {
3410
+ const isKnown = detail.proposals.some((item) => item.id === proposal.id);
3411
+ return {
3412
+ ...detail,
3413
+ proposals: isKnown ? detail.proposals.map((item) => item.id === proposal.id ? proposal : item) : [...detail.proposals, proposal]
3414
+ };
3415
+ }
3416
+ function withSegment(detail, segment) {
3417
+ const others = detail.segments.filter((item) => item.id !== segment.id);
3418
+ return {
3419
+ ...detail,
3420
+ segments: [...others, segment].sort((a, b) => a.startMs - b.startMs)
3421
+ };
3422
+ }
3423
+ function withEvidence(detail, evidence) {
3424
+ return {
3425
+ ...detail,
3426
+ evidence: detail.evidence.map((item) => item.id === evidence.id ? evidence : item)
3427
+ };
3428
+ }
3429
+ function isHidden() {
3430
+ return typeof document !== "undefined" && document.visibilityState === "hidden";
3431
+ }
3432
+ function usePollTick(lastAnswer) {
3433
+ const [tick, setTick] = useState(0);
3434
+ useEffect(() => {
3435
+ if (lastAnswer.kind !== "ready" || !isDetailProcessing(lastAnswer.detail)) return;
3436
+ let timer = 0;
3437
+ const schedule = () => {
3438
+ window.clearTimeout(timer);
3439
+ if (!isHidden()) timer = window.setTimeout(() => setTick((current) => current + 1), POLL_INTERVAL_MS);
3440
+ };
3441
+ schedule();
3442
+ document.addEventListener("visibilitychange", schedule);
3443
+ return () => {
3444
+ window.clearTimeout(timer);
3445
+ document.removeEventListener("visibilitychange", schedule);
3446
+ };
3447
+ }, [lastAnswer]);
3448
+ return tick;
3449
+ }
3450
+ function failedState(error, current) {
3451
+ if (error instanceof SessionApiError && error.status === 404) return { kind: "missing" };
3452
+ if (current.kind === "ready") return current;
3453
+ return {
3454
+ kind: "failed",
3455
+ message: failureMessage(error)
3456
+ };
3457
+ }
3458
+ function useLandedUploadTick() {
3459
+ const { account } = useSessionWorkspace();
3460
+ const { pendingOperations } = useSyncStatus(account.key);
3461
+ const [seen, setSeen] = useState({
3462
+ pending: pendingOperations,
3463
+ tick: 0
3464
+ });
3465
+ if (seen.pending !== pendingOperations) setSeen({
3466
+ pending: pendingOperations,
3467
+ tick: pendingOperations < seen.pending ? seen.tick + 1 : seen.tick
3468
+ });
3469
+ return seen.tick;
3470
+ }
3471
+ function useSessionDetail(sessionId) {
3472
+ const { api, onUnauthorized } = useSessionWorkspace();
3473
+ const [state, setState] = useState({ kind: "loading" });
3474
+ const [request, setRequest] = useState({
3475
+ count: 0,
3476
+ sessionId
3477
+ });
3478
+ const pollTick = usePollTick(state);
3479
+ const uploadTick = useLandedUploadTick();
3480
+ useEffect(() => {
3481
+ let isCurrent = true;
3482
+ api.detail(request.sessionId).then((detail) => isCurrent && setState({
3483
+ detail,
3484
+ kind: "ready"
3485
+ }), (error) => {
3486
+ if (isUnauthorized(error)) {
3487
+ onUnauthorized();
3488
+ return;
3489
+ }
3490
+ if (isCurrent) setState((current) => failedState(error, current));
3491
+ const isRefresh = request.count + pollTick + uploadTick > 0;
3492
+ if (isCurrent && isRefresh) reportFailure("The session did not refresh", error);
3493
+ });
3494
+ return () => {
3495
+ isCurrent = false;
3496
+ };
3497
+ }, [
3498
+ api,
3499
+ onUnauthorized,
3500
+ request,
3501
+ pollTick,
3502
+ uploadTick
3503
+ ]);
3504
+ return {
3505
+ patch: (update) => setState((current) => current.kind === "ready" ? {
3506
+ detail: update(current.detail),
3507
+ kind: "ready"
3508
+ } : current),
3509
+ reload: () => setRequest(nextRequest),
3510
+ state
3511
+ };
3512
+ }
3513
+ //#endregion
3514
+ //#region src/sessions/ui/review/transcript/remove-evidence.tsx
3515
+ const REMOVE_COPY = "Remove this screenshot? The image is deleted for good. Briefs you already downloaded keep their copy.";
3516
+ function useFocusWhen(shouldFocus) {
3517
+ const ref = useRef(null);
3518
+ useEffect(() => {
3519
+ if (shouldFocus) ref.current?.focus();
3520
+ }, [shouldFocus]);
3521
+ return ref;
3522
+ }
3523
+ function RemoveConfirmation({ evidence, onCancel, removal }) {
3524
+ const { api } = useSessionWorkspace();
3525
+ const run = useSessionAction();
3526
+ const cancelRef = useFocusWhen(true);
3527
+ const remove = async () => {
3528
+ const removed = await run("The screenshot was not removed", () => api.removeEvidence(evidence.id));
3529
+ if (removed !== null) {
3530
+ removal.review.patch((detail) => withEvidence(detail, removed));
3531
+ removal.review.reload();
3532
+ removal.onRemoved(removed.id);
3533
+ announce("Screenshot removed");
3534
+ }
3535
+ };
3536
+ return /* @__PURE__ */ jsxs("div", {
3537
+ className: "dt-ses-banner",
3538
+ children: [/* @__PURE__ */ jsx("p", { children: REMOVE_COPY }), /* @__PURE__ */ jsxs("div", {
3539
+ className: "dt-actions",
3540
+ children: [/* @__PURE__ */ jsx("button", {
3541
+ className: "dt-btn",
3542
+ "data-variant": "danger",
3543
+ onClick: remove,
3544
+ type: "button",
3545
+ children: "Remove"
3546
+ }), /* @__PURE__ */ jsx("button", {
3547
+ className: "dt-btn",
3548
+ onClick: onCancel,
3549
+ ref: cancelRef,
3550
+ type: "button",
3551
+ children: "Cancel"
3552
+ })]
3553
+ })]
3554
+ });
3555
+ }
3556
+ function RemoveEvidence({ evidence, removal }) {
3557
+ const [isConfirming, setIsConfirming] = useState(false);
3558
+ const [hasCancelled, setHasCancelled] = useState(false);
3559
+ const triggerRef = useFocusWhen(hasCancelled);
3560
+ if (isConfirming) return /* @__PURE__ */ jsx(RemoveConfirmation, {
3561
+ evidence,
3562
+ onCancel: () => {
3563
+ setIsConfirming(false);
3564
+ setHasCancelled(true);
3565
+ },
3566
+ removal
3567
+ });
3568
+ const label = `Remove screenshot at ${formatClock(evidence.atMs)}`;
3569
+ return /* @__PURE__ */ jsx("button", {
3570
+ "aria-label": label,
3571
+ className: "icon-btn glass dt-ses-hover-action",
3572
+ onClick: () => {
3573
+ setIsConfirming(true);
3574
+ setHasCancelled(false);
3575
+ },
3576
+ ref: triggerRef,
3577
+ title: label,
3578
+ type: "button",
3579
+ children: /* @__PURE__ */ jsx(TrashIcon, {})
3580
+ });
3581
+ }
3582
+ //#endregion
3352
3583
  //#region src/sessions/ui/session-icons.tsx
3353
3584
  function MicIcon() {
3354
3585
  return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("rect", {
@@ -3375,6 +3606,22 @@ function ClockIcon() {
3375
3606
  r: "8.5"
3376
3607
  }), /* @__PURE__ */ jsx("path", { d: "M12 7.5V12l3 2" })] });
3377
3608
  }
3609
+ function LockIcon() {
3610
+ return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("rect", {
3611
+ height: "10",
3612
+ rx: "2.5",
3613
+ width: "15",
3614
+ x: "4.5",
3615
+ y: "10.5"
3616
+ }), /* @__PURE__ */ jsx("path", { d: "M8 10.5V8a4 4 0 0 1 8 0v2.5" })] });
3617
+ }
3618
+ function TeamIcon() {
3619
+ return /* @__PURE__ */ jsxs(Svg, { children: [/* @__PURE__ */ jsx("circle", {
3620
+ cx: "9",
3621
+ cy: "8.5",
3622
+ r: "3.5"
3623
+ }), /* @__PURE__ */ jsx("path", { d: "M2.5 20a6.5 6.5 0 0 1 13 0M16 5.2a3.5 3.5 0 0 1 0 6.6M18.5 14.4a6.5 6.5 0 0 1 3 5.6" })] });
3624
+ }
3378
3625
  function IntentionIcon() {
3379
3626
  return /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.6.5 1.1 1.3 1.1 2.2h5c0-.9.5-1.7 1.1-2.2A6 6 0 0 0 12 3z" }) });
3380
3627
  }
@@ -3399,6 +3646,50 @@ function ImageIcon() {
3399
3646
  ] });
3400
3647
  }
3401
3648
  //#endregion
3649
+ //#region src/sessions/ui/recording/cancel-confirmation.tsx
3650
+ function CancelConfirmation({ onCancelRecording, onKeepRecording }) {
3651
+ const confirmationRef = useRef(null);
3652
+ const keepRef = useFocusWhen(true);
3653
+ useEffect(() => {
3654
+ const confirmation = confirmationRef.current;
3655
+ if (!confirmation) return;
3656
+ const keepOnEscape = (event) => {
3657
+ if (event.key === "Escape" && !event.isComposing) {
3658
+ event.stopPropagation();
3659
+ onKeepRecording();
3660
+ }
3661
+ };
3662
+ confirmation.addEventListener("keydown", keepOnEscape);
3663
+ return () => confirmation.removeEventListener("keydown", keepOnEscape);
3664
+ }, [onKeepRecording]);
3665
+ return /* @__PURE__ */ jsxs("div", {
3666
+ "aria-labelledby": "rf-rec-cancel-copy",
3667
+ className: "dt-rec-note",
3668
+ ref: confirmationRef,
3669
+ role: "alertdialog",
3670
+ children: [
3671
+ /* @__PURE__ */ jsx("span", {
3672
+ id: "rf-rec-cancel-copy",
3673
+ children: "Cancel this recording? Nothing from it is kept."
3674
+ }),
3675
+ /* @__PURE__ */ jsx("button", {
3676
+ className: "dt-btn",
3677
+ "data-variant": "danger",
3678
+ onClick: onCancelRecording,
3679
+ type: "button",
3680
+ children: "Discard"
3681
+ }),
3682
+ /* @__PURE__ */ jsx("button", {
3683
+ className: "dt-btn",
3684
+ onClick: onKeepRecording,
3685
+ ref: keepRef,
3686
+ type: "button",
3687
+ children: "Keep recording"
3688
+ })
3689
+ ]
3690
+ });
3691
+ }
3692
+ //#endregion
3402
3693
  //#region src/sessions/ui/recording/level-meter.tsx
3403
3694
  const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
3404
3695
  /** The level changes many times a second: it goes straight to a CSS variable, never through React state. */
@@ -3461,25 +3752,19 @@ function MarkNote({ focusOnOpen, onClose }) {
3461
3752
  //#endregion
3462
3753
  //#region src/sessions/ui/recording/recording-bar.tsx
3463
3754
  const MARK_HOTKEY = "alt+shift+m";
3464
- function SyncChip() {
3465
- const { account } = useSessionWorkspace();
3466
- const sync = useSyncStatus(account.key);
3467
- let label = "Saved";
3468
- if (sync.blockedBy === "storage") label = "Not saved on this device";
3469
- else if (!sync.isOnline) label = "Offline — kept on this device";
3470
- else if (sync.pendingOperations > 0) label = `${sync.pendingOperations} waiting to upload`;
3471
- return /* @__PURE__ */ jsx("span", {
3472
- className: "dt-chip",
3473
- children: label
3474
- });
3755
+ function syncProblem(sync) {
3756
+ if (sync.blockedBy === "storage") return "Not saved on this device";
3757
+ if (!sync.isOnline) return "Offline — kept on this device";
3758
+ return null;
3475
3759
  }
3476
- function statusText(snapshot, maxDurationMs) {
3760
+ function statusText(snapshot, sync, maxDurationMs) {
3477
3761
  if (snapshot.phase === "failed") return snapshot.failure?.message ?? "Recording stopped.";
3478
3762
  if (snapshot.phase === "finishing") return "Finishing…";
3479
3763
  if (snapshot.pauseReason === "limit") return "Time limit reached · Finish to review the session";
3480
3764
  if (snapshot.pauseReason === "hidden") return "Paused because the tab was hidden";
3481
- if (snapshot.phase === "paused") return "Paused · nothing is recorded";
3482
3765
  if (snapshot.limitsReached.length > 0) return `Limit reached for ${snapshot.limitsReached.join(", ")}`;
3766
+ const problem = syncProblem(sync);
3767
+ if (problem) return problem;
3483
3768
  return snapshot.nearLimit ? `${minutesLeft(maxDurationMs - snapshot.activeMs)} min left` : "";
3484
3769
  }
3485
3770
  function markMoment(recorder, focus, setOpenMark) {
@@ -3513,42 +3798,67 @@ function useMarkMoment(snapshot, barRef) {
3513
3798
  };
3514
3799
  }
3515
3800
  function CaptureButtons({ canCapture, onMark, onPointAt }) {
3516
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
3801
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
3517
3802
  "aria-keyshortcuts": "Alt+Shift+M",
3803
+ "aria-label": "Mark moment",
3518
3804
  className: "dt-btn",
3805
+ "data-icon-only": true,
3519
3806
  disabled: !canCapture,
3520
3807
  onClick: onMark,
3521
3808
  title: "Mark moment (Alt+Shift+M)",
3522
3809
  type: "button",
3523
- children: [/* @__PURE__ */ jsx(MarkIcon, {}), "Mark moment"]
3810
+ children: /* @__PURE__ */ jsx(MarkIcon, {})
3524
3811
  }), /* @__PURE__ */ jsx("button", {
3812
+ "aria-label": "Point at element",
3525
3813
  className: "dt-btn",
3814
+ "data-icon-only": true,
3526
3815
  disabled: !canCapture,
3527
3816
  onClick: onPointAt,
3817
+ title: "Point at element",
3528
3818
  type: "button",
3529
- children: "Point at element"
3819
+ children: /* @__PURE__ */ jsx(TargetIcon, {})
3530
3820
  })] });
3531
3821
  }
3532
3822
  function PauseToggle({ snapshot }) {
3533
3823
  const { recorder } = useSessionWorkspace();
3534
- if (snapshot.phase === "recording") return /* @__PURE__ */ jsxs("button", {
3824
+ if (snapshot.phase === "recording") return /* @__PURE__ */ jsx("button", {
3825
+ "aria-label": "Pause",
3535
3826
  className: "dt-btn",
3827
+ "data-icon-only": true,
3536
3828
  onClick: () => recorder.pause("paused"),
3829
+ title: "Pause",
3537
3830
  type: "button",
3538
- children: [/* @__PURE__ */ jsx(PauseIcon, {}), "Pause"]
3831
+ children: /* @__PURE__ */ jsx(PauseIcon, {})
3539
3832
  });
3540
- if (snapshot.phase === "paused" && snapshot.pauseReason !== "limit") return /* @__PURE__ */ jsxs("button", {
3833
+ if (snapshot.phase === "paused" && snapshot.pauseReason !== "limit") return /* @__PURE__ */ jsx("button", {
3834
+ "aria-label": "Resume",
3541
3835
  className: "dt-btn",
3836
+ "data-icon-only": true,
3542
3837
  onClick: () => recorder.resume(),
3838
+ title: "Resume",
3543
3839
  type: "button",
3544
- children: [/* @__PURE__ */ jsx(PlayIcon, {}), "Resume"]
3840
+ children: /* @__PURE__ */ jsx(PlayIcon, {})
3545
3841
  });
3546
3842
  return null;
3547
3843
  }
3548
- function RecordingControls({ onFinish, onMark, onPointAt, snapshot }) {
3844
+ function CancelButton({ hasKeptRecording, onAskToCancel }) {
3845
+ const buttonRef = useFocusWhen(hasKeptRecording);
3846
+ return /* @__PURE__ */ jsx("button", {
3847
+ "aria-label": "Cancel recording",
3848
+ className: "dt-btn",
3849
+ "data-icon-only": true,
3850
+ onClick: onAskToCancel,
3851
+ ref: buttonRef,
3852
+ title: "Cancel recording",
3853
+ type: "button",
3854
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
3855
+ });
3856
+ }
3857
+ function RecordingControls({ hasKeptRecording, onAskToCancel, onFinish, onMark, onPointAt, snapshot }) {
3549
3858
  const { phase } = snapshot;
3859
+ const isActive = phase === "recording" || phase === "paused";
3550
3860
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3551
- (phase === "recording" || phase === "paused") && /* @__PURE__ */ jsx(CaptureButtons, {
3861
+ isActive && /* @__PURE__ */ jsx(CaptureButtons, {
3552
3862
  canCapture: phase === "recording",
3553
3863
  onMark,
3554
3864
  onPointAt
@@ -3561,6 +3871,10 @@ function RecordingControls({ onFinish, onMark, onPointAt, snapshot }) {
3561
3871
  onClick: onFinish,
3562
3872
  type: "button",
3563
3873
  children: "Finish"
3874
+ }),
3875
+ isActive && /* @__PURE__ */ jsx(CancelButton, {
3876
+ hasKeptRecording,
3877
+ onAskToCancel
3564
3878
  })
3565
3879
  ] });
3566
3880
  }
@@ -3573,10 +3887,14 @@ function RecordingClock({ activeMs }) {
3573
3887
  children: formatClock(activeMs)
3574
3888
  });
3575
3889
  }
3576
- function RecordingBar({ onFinish, onPointAt, snapshot }) {
3577
- const { identity, recorder } = useSessionWorkspace();
3890
+ function RecordingBar({ onCancel, onFinish, onPointAt, snapshot }) {
3891
+ const { account, identity, recorder } = useSessionWorkspace();
3892
+ const sync = useSyncStatus(account.key);
3578
3893
  const barRef = useRef(null);
3579
3894
  const { markWithNote, openMark } = useMarkMoment(snapshot, barRef);
3895
+ const [cancelStep, setCancelStep] = useState("idle");
3896
+ const keepRecording = useCallback(() => setCancelStep("kept"), []);
3897
+ const showsMeter = snapshot.phase === "recording" || snapshot.phase === "paused";
3580
3898
  return /* @__PURE__ */ jsxs("div", {
3581
3899
  "aria-label": "Session recording",
3582
3900
  className: "dt-rec glass",
@@ -3588,24 +3906,29 @@ function RecordingBar({ onFinish, onPointAt, snapshot }) {
3588
3906
  "aria-hidden": "true",
3589
3907
  className: "dt-rec-dot"
3590
3908
  }),
3591
- snapshot.phase === "recording" && /* @__PURE__ */ jsx(LevelMeter, { recorder }),
3909
+ showsMeter && /* @__PURE__ */ jsx(LevelMeter, { recorder }),
3592
3910
  /* @__PURE__ */ jsx(RecordingClock, { activeMs: snapshot.activeMs }),
3593
3911
  /* @__PURE__ */ jsx("span", {
3594
3912
  "aria-live": "polite",
3595
3913
  className: "dt-rec-status",
3596
- children: statusText(snapshot, identity.limits.maxDurationMs)
3914
+ children: statusText(snapshot, sync, identity.limits.maxDurationMs)
3597
3915
  }),
3598
3916
  /* @__PURE__ */ jsx(RecordingControls, {
3917
+ hasKeptRecording: cancelStep === "kept",
3918
+ onAskToCancel: () => setCancelStep("confirming"),
3599
3919
  onFinish,
3600
3920
  onMark: markWithNote,
3601
3921
  onPointAt,
3602
3922
  snapshot
3603
3923
  }),
3604
- /* @__PURE__ */ jsx(SyncChip, {}),
3605
3924
  openMark && /* @__PURE__ */ jsx(MarkNote, {
3606
3925
  focusOnOpen: openMark.focus,
3607
3926
  onClose: openMark.closeNote
3608
- }, openMark.key)
3927
+ }, openMark.key),
3928
+ cancelStep === "confirming" && /* @__PURE__ */ jsx(CancelConfirmation, {
3929
+ onCancelRecording: onCancel,
3930
+ onKeepRecording: keepRecording
3931
+ })
3609
3932
  ]
3610
3933
  });
3611
3934
  }
@@ -3641,7 +3964,16 @@ function RecordingLayer({ onFinished }) {
3641
3964
  reportFailure("The session did not finish", error, finish);
3642
3965
  }
3643
3966
  };
3967
+ const cancel = async () => {
3968
+ setIsPointing(false);
3969
+ try {
3970
+ await recorder.cancel();
3971
+ } catch (error) {
3972
+ reportFailure("The recording was not deleted", error);
3973
+ }
3974
+ };
3644
3975
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(RecordingBar, {
3976
+ onCancel: cancel,
3645
3977
  onFinish: finish,
3646
3978
  onPointAt: () => setIsPointing(true),
3647
3979
  snapshot
@@ -3824,6 +4156,15 @@ function evidenceCaption(evidence) {
3824
4156
  const element = evidence.element ? ` · ${evidence.element.label}` : "";
3825
4157
  return `${TRIGGER_LABELS[evidence.trigger]}${element}`;
3826
4158
  }
4159
+ function ShotCaption({ atMs, children }) {
4160
+ return /* @__PURE__ */ jsxs("span", {
4161
+ className: "dt-ses-caption",
4162
+ children: [/* @__PURE__ */ jsxs("span", {
4163
+ className: "dt-ses-caption-time",
4164
+ children: [/* @__PURE__ */ jsx(ClockIcon, {}), formatClock(atMs)]
4165
+ }), children]
4166
+ });
4167
+ }
3827
4168
  const hasViewport = (frame) => frame.viewportWidth > 0 && frame.viewportHeight > 0;
3828
4169
  const percent = (value, total) => `${value / total * 100}%`;
3829
4170
  function percentBox(rect, frame) {
@@ -4363,119 +4704,6 @@ function SaveProblemMessage({ onReload, problem }) {
4363
4704
  });
4364
4705
  }
4365
4706
  //#endregion
4366
- //#region src/sessions/ui/review/use-session-detail.ts
4367
- const POLL_INTERVAL_MS = 2e3;
4368
- function nextRequest(current) {
4369
- return {
4370
- ...current,
4371
- count: current.count + 1
4372
- };
4373
- }
4374
- function withProposal(detail, proposal) {
4375
- const isKnown = detail.proposals.some((item) => item.id === proposal.id);
4376
- return {
4377
- ...detail,
4378
- proposals: isKnown ? detail.proposals.map((item) => item.id === proposal.id ? proposal : item) : [...detail.proposals, proposal]
4379
- };
4380
- }
4381
- function withSegment(detail, segment) {
4382
- const others = detail.segments.filter((item) => item.id !== segment.id);
4383
- return {
4384
- ...detail,
4385
- segments: [...others, segment].sort((a, b) => a.startMs - b.startMs)
4386
- };
4387
- }
4388
- function withEvidence(detail, evidence) {
4389
- return {
4390
- ...detail,
4391
- evidence: detail.evidence.map((item) => item.id === evidence.id ? evidence : item)
4392
- };
4393
- }
4394
- function isHidden() {
4395
- return typeof document !== "undefined" && document.visibilityState === "hidden";
4396
- }
4397
- function usePollTick(lastAnswer) {
4398
- const [tick, setTick] = useState(0);
4399
- useEffect(() => {
4400
- if (lastAnswer.kind !== "ready" || !isDetailProcessing(lastAnswer.detail)) return;
4401
- let timer = 0;
4402
- const schedule = () => {
4403
- window.clearTimeout(timer);
4404
- if (!isHidden()) timer = window.setTimeout(() => setTick((current) => current + 1), POLL_INTERVAL_MS);
4405
- };
4406
- schedule();
4407
- document.addEventListener("visibilitychange", schedule);
4408
- return () => {
4409
- window.clearTimeout(timer);
4410
- document.removeEventListener("visibilitychange", schedule);
4411
- };
4412
- }, [lastAnswer]);
4413
- return tick;
4414
- }
4415
- function failedState(error, current) {
4416
- if (error instanceof SessionApiError && error.status === 404) return { kind: "missing" };
4417
- if (current.kind === "ready") return current;
4418
- return {
4419
- kind: "failed",
4420
- message: failureMessage(error)
4421
- };
4422
- }
4423
- function useLandedUploadTick() {
4424
- const { account } = useSessionWorkspace();
4425
- const { pendingOperations } = useSyncStatus(account.key);
4426
- const [seen, setSeen] = useState({
4427
- pending: pendingOperations,
4428
- tick: 0
4429
- });
4430
- if (seen.pending !== pendingOperations) setSeen({
4431
- pending: pendingOperations,
4432
- tick: pendingOperations < seen.pending ? seen.tick + 1 : seen.tick
4433
- });
4434
- return seen.tick;
4435
- }
4436
- function useSessionDetail(sessionId) {
4437
- const { api, onUnauthorized } = useSessionWorkspace();
4438
- const [state, setState] = useState({ kind: "loading" });
4439
- const [request, setRequest] = useState({
4440
- count: 0,
4441
- sessionId
4442
- });
4443
- const pollTick = usePollTick(state);
4444
- const uploadTick = useLandedUploadTick();
4445
- useEffect(() => {
4446
- let isCurrent = true;
4447
- api.detail(request.sessionId).then((detail) => isCurrent && setState({
4448
- detail,
4449
- kind: "ready"
4450
- }), (error) => {
4451
- if (isUnauthorized(error)) {
4452
- onUnauthorized();
4453
- return;
4454
- }
4455
- if (isCurrent) setState((current) => failedState(error, current));
4456
- const isRefresh = request.count + pollTick + uploadTick > 0;
4457
- if (isCurrent && isRefresh) reportFailure("The session did not refresh", error);
4458
- });
4459
- return () => {
4460
- isCurrent = false;
4461
- };
4462
- }, [
4463
- api,
4464
- onUnauthorized,
4465
- request,
4466
- pollTick,
4467
- uploadTick
4468
- ]);
4469
- return {
4470
- patch: (update) => setState((current) => current.kind === "ready" ? {
4471
- detail: update(current.detail),
4472
- kind: "ready"
4473
- } : current),
4474
- reload: () => setRequest(nextRequest),
4475
- state
4476
- };
4477
- }
4478
- //#endregion
4479
4707
  //#region src/sessions/ui/review/proposals/proposal-editor.tsx
4480
4708
  const MAX_LIST_ITEMS = 8;
4481
4709
  function lines(text) {
@@ -5252,73 +5480,6 @@ function UpdateCard({ context, continuation }) {
5252
5480
  });
5253
5481
  }
5254
5482
  //#endregion
5255
- //#region src/sessions/ui/review/transcript/remove-evidence.tsx
5256
- const REMOVE_COPY = "Remove this screenshot? The image is deleted for good. Briefs you already downloaded keep their copy.";
5257
- function useFocusWhen(shouldFocus) {
5258
- const ref = useRef(null);
5259
- useEffect(() => {
5260
- if (shouldFocus) ref.current?.focus();
5261
- }, [shouldFocus]);
5262
- return ref;
5263
- }
5264
- function RemoveConfirmation({ evidence, onCancel, removal }) {
5265
- const { api } = useSessionWorkspace();
5266
- const run = useSessionAction();
5267
- const cancelRef = useFocusWhen(true);
5268
- const remove = async () => {
5269
- const removed = await run("The screenshot was not removed", () => api.removeEvidence(evidence.id));
5270
- if (removed !== null) {
5271
- removal.review.patch((detail) => withEvidence(detail, removed));
5272
- removal.review.reload();
5273
- removal.onRemoved(removed.id);
5274
- announce("Screenshot removed");
5275
- }
5276
- };
5277
- return /* @__PURE__ */ jsxs("div", {
5278
- className: "dt-ses-banner",
5279
- children: [/* @__PURE__ */ jsx("p", { children: REMOVE_COPY }), /* @__PURE__ */ jsxs("div", {
5280
- className: "dt-actions",
5281
- children: [/* @__PURE__ */ jsx("button", {
5282
- className: "dt-btn",
5283
- "data-variant": "danger",
5284
- onClick: remove,
5285
- type: "button",
5286
- children: "Remove"
5287
- }), /* @__PURE__ */ jsx("button", {
5288
- className: "dt-btn",
5289
- onClick: onCancel,
5290
- ref: cancelRef,
5291
- type: "button",
5292
- children: "Cancel"
5293
- })]
5294
- })]
5295
- });
5296
- }
5297
- function RemoveEvidence({ evidence, removal }) {
5298
- const [isConfirming, setIsConfirming] = useState(false);
5299
- const [hasCancelled, setHasCancelled] = useState(false);
5300
- const triggerRef = useFocusWhen(hasCancelled);
5301
- if (isConfirming) return /* @__PURE__ */ jsx(RemoveConfirmation, {
5302
- evidence,
5303
- onCancel: () => {
5304
- setIsConfirming(false);
5305
- setHasCancelled(true);
5306
- },
5307
- removal
5308
- });
5309
- return /* @__PURE__ */ jsx("button", {
5310
- "aria-label": `Remove screenshot at ${formatClock(evidence.atMs)}`,
5311
- className: "dt-ses-link",
5312
- onClick: () => {
5313
- setIsConfirming(true);
5314
- setHasCancelled(false);
5315
- },
5316
- ref: triggerRef,
5317
- type: "button",
5318
- children: "Remove"
5319
- });
5320
- }
5321
- //#endregion
5322
5483
  //#region src/sessions/ui/review/moment/moment-labels.ts
5323
5484
  const URL_PATH = /^[a-z][a-z\d+.-]*:\/\/[^/?#]*([^?#]*)/i;
5324
5485
  const KIND_LABELS = {
@@ -5338,7 +5499,7 @@ function contextKindLabel(item) {
5338
5499
  return KIND_LABELS[item.kind];
5339
5500
  }
5340
5501
  function pagePath(url) {
5341
- if (!url) return null;
5502
+ if (!url || url.startsWith("file:")) return null;
5342
5503
  return URL_PATH.exec(url)?.[1] || "/";
5343
5504
  }
5344
5505
  function contextLine$1(item) {
@@ -5852,70 +6013,263 @@ function MomentView({ handlers, segmentId, session }) {
5852
6013
  });
5853
6014
  }
5854
6015
  //#endregion
5855
- //#region src/sessions/ui/review/screens/callout-report.ts
5856
- const clampFraction = (value) => Math.min(1, Math.max(0, value));
5857
- function visibleFraction(rect, frame) {
5858
- if (frame.viewportWidth <= 0 || frame.viewportHeight <= 0) return null;
5859
- const left = clampFraction(rect.x / frame.viewportWidth);
5860
- const top = clampFraction(rect.y / frame.viewportHeight);
5861
- const right = clampFraction((rect.x + rect.width) / frame.viewportWidth);
5862
- const bottom = clampFraction((rect.y + rect.height) / frame.viewportHeight);
5863
- if (right <= left || bottom <= top) return null;
5864
- return {
5865
- height: bottom - top,
5866
- width: right - left,
5867
- x: left,
5868
- y: top
6016
+ //#region src/sessions/ui/review/processing-steps.ts
6017
+ function uploadStep(pendingUploads) {
6018
+ return pendingUploads > 0 ? {
6019
+ analyzeLabel: null,
6020
+ key: "upload",
6021
+ label: "Uploading",
6022
+ state: "active"
6023
+ } : {
6024
+ analyzeLabel: null,
6025
+ key: "upload",
6026
+ label: "Uploaded",
6027
+ state: "done"
5869
6028
  };
5870
6029
  }
5871
- function shownScreenshots(detail) {
5872
- const shots = /* @__PURE__ */ new Map();
5873
- for (const evidence of detail.evidence) {
5874
- const url = evidence.status === "available" ? evidence.image?.url : null;
5875
- if (evidence.image && url) shots.set(evidence.id, {
5876
- evidence,
5877
- image: {
5878
- ...evidence.image,
5879
- url
5880
- }
5881
- });
5882
- }
5883
- return shots;
5884
- }
5885
- const readingOrderY = (callout) => callout.outline?.y ?? 0;
5886
- function calloutReport(detail) {
5887
- const shots = shownScreenshots(detail);
5888
- const screensById = /* @__PURE__ */ new Map();
5889
- const unplaced = [];
5890
- const reported = detail.proposals.filter((proposal) => proposal.mergedIntoId === null && proposal.attachment === null);
5891
- for (const proposal of reported) {
5892
- const label = proposal.callout ?? proposal.need;
5893
- const target = proposal.calloutTarget;
5894
- const shot = target ? shots.get(target.evidenceId) : void 0;
5895
- if (!(target && shot)) {
5896
- unplaced.push({
5897
- label,
5898
- outline: null,
5899
- proposal
5900
- });
5901
- continue;
5902
- }
5903
- const { frame } = shot.evidence;
5904
- const outline = target.element && frame ? visibleFraction(target.element.rect, frame) : null;
5905
- const screen = screensById.get(shot.evidence.id) ?? {
5906
- ...shot,
5907
- callouts: []
6030
+ function transcriptionStep(detail, isAnalysisQueued) {
6031
+ const voice = detail.segments.filter((segment) => segment.source === "voice");
6032
+ const transcribing = voice.filter(({ transcription }) => transcription.state === "pending" || transcription.state === "running").length;
6033
+ const step = {
6034
+ analyzeLabel: null,
6035
+ key: "transcription"
6036
+ };
6037
+ if (transcribing > 0) {
6038
+ const transcribed = voice.length - transcribing;
6039
+ return {
6040
+ ...step,
6041
+ label: "Transcribing your voice",
6042
+ progress: {
6043
+ done: transcribed,
6044
+ total: voice.length,
6045
+ valueText: `${transcribed} of ${countLabel(voice.length, "passage")}`
6046
+ },
6047
+ state: "active"
5908
6048
  };
5909
- screen.callouts.push({
5910
- label,
5911
- outline,
5912
- proposal
5913
- });
5914
- screensById.set(shot.evidence.id, screen);
5915
6049
  }
5916
- let number = 0;
5917
- const numbered = (callout) => {
5918
- number += 1;
6050
+ if (voice.length > 0) return {
6051
+ ...step,
6052
+ label: "Voice transcribed",
6053
+ state: "done"
6054
+ };
6055
+ return isAnalysisQueued ? {
6056
+ ...step,
6057
+ label: "Transcribe your voice",
6058
+ state: "waiting"
6059
+ } : null;
6060
+ }
6061
+ function comparedRevision(detail) {
6062
+ const { continuation } = detail;
6063
+ return continuation?.intention && continuation.contribution.detachedAt === null ? continuation.contribution.baseRevision : null;
6064
+ }
6065
+ function intentionStep(detail, isAnalysisQueued) {
6066
+ const { session } = detail;
6067
+ const { analysis } = session;
6068
+ const revision = comparedRevision(detail);
6069
+ const isComparing = revision !== null;
6070
+ const step = {
6071
+ analyzeLabel: null,
6072
+ key: "intentions"
6073
+ };
6074
+ if (analysis.state === "running") {
6075
+ const label = isComparing ? `Comparing with revision ${revision}…` : "Finding intentions with AI…";
6076
+ return {
6077
+ ...step,
6078
+ label,
6079
+ state: "active"
6080
+ };
6081
+ }
6082
+ if (analysis.state === "waiting" || isAnalysisQueued) {
6083
+ const label = isComparing ? `Compare with revision ${revision}` : "Find intentions with AI";
6084
+ return {
6085
+ ...step,
6086
+ label,
6087
+ state: "waiting"
6088
+ };
6089
+ }
6090
+ if (analysis.state === "failed") {
6091
+ const failure = isComparing ? "The comparison failed" : "Analysis failed";
6092
+ const reason = analysis.error ?? "Unknown error.";
6093
+ return {
6094
+ ...step,
6095
+ analyzeLabel: "Try again",
6096
+ label: `${failure}: ${reason}`,
6097
+ state: "failed"
6098
+ };
6099
+ }
6100
+ if (isAnalysisOutdated(session)) return {
6101
+ ...step,
6102
+ analyzeLabel: "Analyze again",
6103
+ label: "The transcript changed since the analysis.",
6104
+ state: "waiting"
6105
+ };
6106
+ if (analysis.state === "idle") return detail.segments.length > 0 ? {
6107
+ ...step,
6108
+ analyzeLabel: "Analyze",
6109
+ label: "Not analyzed yet.",
6110
+ state: "waiting"
6111
+ } : null;
6112
+ const label = isComparing ? `Compared with revision ${revision}` : `Found ${countLabel(analysis.proposalCount ?? 0, "intention")}`;
6113
+ return {
6114
+ ...step,
6115
+ label,
6116
+ state: "done"
6117
+ };
6118
+ }
6119
+ function processingSteps(detail, pendingUploads) {
6120
+ if (detail.session.collection !== "ended") return [];
6121
+ const isAnalysisQueued = pendingUploads > 0 && detail.session.analysis.state === "idle";
6122
+ return [
6123
+ detail.session.isMine ? uploadStep(pendingUploads) : null,
6124
+ transcriptionStep(detail, isAnalysisQueued),
6125
+ intentionStep(detail, isAnalysisQueued)
6126
+ ].filter((step) => step !== null);
6127
+ }
6128
+ //#endregion
6129
+ //#region src/sessions/ui/review/processing-progress.tsx
6130
+ function useProcessingSteps(detail) {
6131
+ const { account } = useSessionWorkspace();
6132
+ const sync = useSyncStatus(account.key);
6133
+ return processingSteps(detail, detail.session.isMine ? sync.pendingOperations : 0);
6134
+ }
6135
+ function AnalyzeButton({ detail, label, review }) {
6136
+ const { api } = useSessionWorkspace();
6137
+ const run = useSessionAction();
6138
+ const analyze = async () => {
6139
+ const status = await run("The analysis did not start", () => api.analyze(detail.session.id));
6140
+ if (status) review.patch((current) => ({
6141
+ ...current,
6142
+ session: {
6143
+ ...current.session,
6144
+ analysis: status
6145
+ }
6146
+ }));
6147
+ };
6148
+ return /* @__PURE__ */ jsx("button", {
6149
+ className: "dt-btn",
6150
+ onClick: analyze,
6151
+ type: "button",
6152
+ children: label
6153
+ });
6154
+ }
6155
+ function ProgressBar({ label, progress }) {
6156
+ if (!progress) return /* @__PURE__ */ jsx("span", {
6157
+ "aria-label": label,
6158
+ className: "dt-ses-bar",
6159
+ role: "progressbar",
6160
+ children: /* @__PURE__ */ jsx("span", { "data-indeterminate": "true" })
6161
+ });
6162
+ const { done, total, valueText } = progress;
6163
+ return /* @__PURE__ */ jsx("span", {
6164
+ "aria-label": label,
6165
+ "aria-valuemax": total,
6166
+ "aria-valuemin": 0,
6167
+ "aria-valuenow": done,
6168
+ "aria-valuetext": valueText,
6169
+ className: "dt-ses-bar",
6170
+ role: "progressbar",
6171
+ title: valueText,
6172
+ children: /* @__PURE__ */ jsx("span", { style: { width: `${done / total * 100}%` } })
6173
+ });
6174
+ }
6175
+ function StepRow({ detail, review, step }) {
6176
+ return /* @__PURE__ */ jsxs("li", {
6177
+ "data-state": step.state,
6178
+ children: [
6179
+ /* @__PURE__ */ jsx("span", { children: step.label }),
6180
+ step.analyzeLabel && detail.session.canEdit && /* @__PURE__ */ jsx(AnalyzeButton, {
6181
+ detail,
6182
+ label: step.analyzeLabel,
6183
+ review
6184
+ }),
6185
+ step.state === "active" && /* @__PURE__ */ jsx(ProgressBar, {
6186
+ label: step.label,
6187
+ progress: step.progress
6188
+ })
6189
+ ]
6190
+ });
6191
+ }
6192
+ function ProcessingProgress({ detail, fillsPanel, isProcessing, review, steps }) {
6193
+ return /* @__PURE__ */ jsx("div", {
6194
+ "aria-live": "polite",
6195
+ className: "dt-ses-progress",
6196
+ "data-fills-panel": fillsPanel,
6197
+ children: isProcessing && /* @__PURE__ */ jsx("ol", {
6198
+ "aria-label": "Processing",
6199
+ className: "dt-ses-steps",
6200
+ children: steps.map((step) => /* @__PURE__ */ jsx(StepRow, {
6201
+ detail,
6202
+ review,
6203
+ step
6204
+ }, step.key))
6205
+ })
6206
+ });
6207
+ }
6208
+ //#endregion
6209
+ //#region src/sessions/ui/review/screens/callout-report.ts
6210
+ const clampFraction = (value) => Math.min(1, Math.max(0, value));
6211
+ function visibleFraction(rect, frame) {
6212
+ if (frame.viewportWidth <= 0 || frame.viewportHeight <= 0) return null;
6213
+ const left = clampFraction(rect.x / frame.viewportWidth);
6214
+ const top = clampFraction(rect.y / frame.viewportHeight);
6215
+ const right = clampFraction((rect.x + rect.width) / frame.viewportWidth);
6216
+ const bottom = clampFraction((rect.y + rect.height) / frame.viewportHeight);
6217
+ if (right <= left || bottom <= top) return null;
6218
+ return {
6219
+ height: bottom - top,
6220
+ width: right - left,
6221
+ x: left,
6222
+ y: top
6223
+ };
6224
+ }
6225
+ function shownScreenshots(detail) {
6226
+ const shots = /* @__PURE__ */ new Map();
6227
+ for (const evidence of detail.evidence) {
6228
+ const url = evidence.status === "available" ? evidence.image?.url : null;
6229
+ if (evidence.image && url) shots.set(evidence.id, {
6230
+ evidence,
6231
+ image: {
6232
+ ...evidence.image,
6233
+ url
6234
+ }
6235
+ });
6236
+ }
6237
+ return shots;
6238
+ }
6239
+ const readingOrderY = (callout) => callout.outline?.y ?? 0;
6240
+ function calloutReport(detail) {
6241
+ const shots = shownScreenshots(detail);
6242
+ const screensById = /* @__PURE__ */ new Map();
6243
+ const unplaced = [];
6244
+ const reported = detail.proposals.filter((proposal) => proposal.mergedIntoId === null && proposal.attachment === null);
6245
+ for (const proposal of reported) {
6246
+ const label = proposal.callout ?? proposal.need;
6247
+ const target = proposal.calloutTarget;
6248
+ const shot = target ? shots.get(target.evidenceId) : void 0;
6249
+ if (!(target && shot)) {
6250
+ unplaced.push({
6251
+ label,
6252
+ outline: null,
6253
+ proposal
6254
+ });
6255
+ continue;
6256
+ }
6257
+ const { frame } = shot.evidence;
6258
+ const outline = target.element && frame ? visibleFraction(target.element.rect, frame) : null;
6259
+ const screen = screensById.get(shot.evidence.id) ?? {
6260
+ ...shot,
6261
+ callouts: []
6262
+ };
6263
+ screen.callouts.push({
6264
+ label,
6265
+ outline,
6266
+ proposal
6267
+ });
6268
+ screensById.set(shot.evidence.id, screen);
6269
+ }
6270
+ let number = 0;
6271
+ const numbered = (callout) => {
6272
+ number += 1;
5919
6273
  return {
5920
6274
  ...callout,
5921
6275
  number
@@ -6029,10 +6383,154 @@ function layoutCallouts(options) {
6029
6383
  };
6030
6384
  }
6031
6385
  return {
6032
- height: imageTop + image.height + bandHeight(belowRows),
6033
- imageTop,
6034
- lines,
6035
- pills
6386
+ height: imageTop + image.height + bandHeight(belowRows),
6387
+ imageTop,
6388
+ lines,
6389
+ pills
6390
+ };
6391
+ }
6392
+ //#endregion
6393
+ //#region src/sessions/ui/review/optimistic-change.ts
6394
+ function useOptimisticChange(review) {
6395
+ const run = useSessionAction();
6396
+ const applyChange = async (change) => {
6397
+ review.patch(change.show);
6398
+ const saved = await run(change.failureLabel, change.request, () => {
6399
+ applyChange(change);
6400
+ });
6401
+ if (saved === null) {
6402
+ review.patch(change.rollback);
6403
+ return null;
6404
+ }
6405
+ review.patch((detail) => change.settle(detail, saved));
6406
+ return saved;
6407
+ };
6408
+ return applyChange;
6409
+ }
6410
+ //#endregion
6411
+ //#region src/sessions/ui/review/proposals/use-proposal-actions.ts
6412
+ const NOTICE_NEED_LENGTH = 60;
6413
+ const DECISION_NOTICE = {
6414
+ dismissed: "Dismissed",
6415
+ kept: "Confirmed",
6416
+ later: "Saved for later",
6417
+ pending: "Back to undecided"
6418
+ };
6419
+ function quotedNeed(need) {
6420
+ return `“${need.length > NOTICE_NEED_LENGTH ? `${need.slice(0, 59)}…` : need}”`;
6421
+ }
6422
+ function decisionToggle(actions, proposal, decision) {
6423
+ return (event) => {
6424
+ if (event.detail > 1) return;
6425
+ actions.decide([proposal], proposal.decision === decision ? "pending" : decision);
6426
+ };
6427
+ }
6428
+ function useProposalSave(review) {
6429
+ const { api } = useSessionWorkspace();
6430
+ const run = useSessionAction();
6431
+ return async (failureLabel, request) => {
6432
+ const proposal = await run(failureLabel, () => request(api));
6433
+ if (proposal) review.patch((detail) => withProposal(detail, proposal));
6434
+ return proposal;
6435
+ };
6436
+ }
6437
+ function withDecisions(decisions) {
6438
+ return (detail) => ({
6439
+ ...detail,
6440
+ proposals: detail.proposals.map((item) => {
6441
+ const decision = decisions.get(item.id);
6442
+ return decision ? {
6443
+ ...item,
6444
+ decision
6445
+ } : item;
6446
+ })
6447
+ });
6448
+ }
6449
+ function decisionNotice(proposals, decision) {
6450
+ const [only] = proposals;
6451
+ const subject = proposals.length === 1 && only ? quotedNeed(only.need) : countLabel(proposals.length, "intention");
6452
+ return `${DECISION_NOTICE[decision]} ${subject}`;
6453
+ }
6454
+ function useDecide(review) {
6455
+ const { api } = useSessionWorkspace();
6456
+ const applyChange = useOptimisticChange(review);
6457
+ const changeDecisions = async (failureLabel, from, to) => {
6458
+ const saved = await applyChange({
6459
+ failureLabel,
6460
+ request: () => Promise.all([...to].map(([proposalId, decision]) => api.decideProposal({
6461
+ decision,
6462
+ proposalId
6463
+ }))),
6464
+ rollback: withDecisions(from),
6465
+ settle: (detail, proposals) => proposals.reduce(withProposal, detail),
6466
+ show: withDecisions(to)
6467
+ });
6468
+ if (!saved && to.size > 1) review.reload();
6469
+ return saved;
6470
+ };
6471
+ return async (proposals, decision) => {
6472
+ const previous = new Map(proposals.map((proposal) => [proposal.id, proposal.decision]));
6473
+ const next = new Map(proposals.map((proposal) => [proposal.id, decision]));
6474
+ if (!await changeDecisions("The decision was not saved", previous, next)) return;
6475
+ announce(decisionNotice(proposals, decision), {
6476
+ label: "Undo",
6477
+ run: async () => {
6478
+ if (await changeDecisions("The decision was not undone", next, previous)) announce("Decision undone");
6479
+ }
6480
+ });
6481
+ };
6482
+ }
6483
+ function withFeedbackIds(feedbackIds) {
6484
+ return (detail) => ({
6485
+ ...detail,
6486
+ proposals: detail.proposals.map((item) => {
6487
+ const feedbackId = feedbackIds.get(item.id);
6488
+ return feedbackId ? {
6489
+ ...item,
6490
+ feedbackId
6491
+ } : item;
6492
+ })
6493
+ });
6494
+ }
6495
+ function useAddToBoard(review) {
6496
+ const { api } = useSessionWorkspace();
6497
+ const run = useSessionAction();
6498
+ return async (proposals) => {
6499
+ const created = await Promise.all(proposals.map(async (proposal) => {
6500
+ const result = await run(`${quotedNeed(proposal.need)} was not added to the board`, () => api.createFeedback(proposal.id));
6501
+ return result ? [proposal.id, result.feedbackId] : null;
6502
+ }));
6503
+ const added = new Map(created.flatMap((entry) => entry ? [entry] : []));
6504
+ if (added.size === 0) return;
6505
+ review.patch(withFeedbackIds(added));
6506
+ const onlyAdded = added.size === 1 ? proposals.find((proposal) => added.has(proposal.id)) : void 0;
6507
+ announce(onlyAdded ? `Added ${quotedNeed(onlyAdded.need)} to the board` : `Added ${countLabel(added.size, "intention")} to the board`);
6508
+ };
6509
+ }
6510
+ function useProposalActions(review) {
6511
+ const save = useProposalSave(review);
6512
+ const decide = useDecide(review);
6513
+ return {
6514
+ addToBoard: useAddToBoard(review),
6515
+ clarify: (proposal, text) => save("The precision was not sent", (client) => client.clarifyProposal({
6516
+ expectedRevision: proposal.revision,
6517
+ proposalId: proposal.id,
6518
+ text
6519
+ })),
6520
+ decide,
6521
+ merge: async (targetId, sourceIds) => {
6522
+ if (await save("The intentions were not merged", (client) => client.mergeProposals({
6523
+ sourceIds,
6524
+ targetId
6525
+ }))) review.reload();
6526
+ },
6527
+ save,
6528
+ separate: (proposalId) => save("The suggestion was not cleared", (client) => client.separateProposal(proposalId)),
6529
+ verify: (proposalId, { note, status }) => save("The check was not saved", (client) => client.verifyProposal({
6530
+ note: note || void 0,
6531
+ proposalId,
6532
+ status
6533
+ }))
6036
6534
  };
6037
6535
  }
6038
6536
  //#endregion
@@ -6065,22 +6563,16 @@ function CalloutPill({ callout, context, pillRef, style }) {
6065
6563
  type: "button",
6066
6564
  children: label
6067
6565
  }),
6068
- context.canEdit && TOGGLES.map(({ decision, icon, verb }) => {
6069
- const isOn = proposal.decision === decision;
6070
- return /* @__PURE__ */ jsx("button", {
6071
- "aria-disabled": isOn,
6072
- "aria-label": `${verb} “${label}”`,
6073
- "aria-pressed": isOn,
6074
- className: "dt-ses-callout-btn",
6075
- "data-decision": decision,
6076
- onClick: () => {
6077
- if (!isOn) context.actions.decide(proposal, decision);
6078
- },
6079
- title: verb,
6080
- type: "button",
6081
- children: icon
6082
- }, decision);
6083
- })
6566
+ context.canEdit && TOGGLES.map(({ decision, icon, verb }) => /* @__PURE__ */ jsx("button", {
6567
+ "aria-label": `${verb} “${label}”`,
6568
+ "aria-pressed": proposal.decision === decision,
6569
+ className: "dt-ses-callout-btn",
6570
+ "data-decision": decision,
6571
+ onClick: decisionToggle(context.actions, proposal, decision),
6572
+ title: verb,
6573
+ type: "button",
6574
+ children: icon
6575
+ }, decision))
6084
6576
  ]
6085
6577
  });
6086
6578
  }
@@ -6121,12 +6613,6 @@ function useMeasuredPills(measureKey) {
6121
6613
  pillRef
6122
6614
  };
6123
6615
  }
6124
- function screenCaption(screen) {
6125
- const { evidence } = screen;
6126
- const time = formatClock(evidence.capturedAtMs ?? evidence.atMs);
6127
- const page = pagePath(evidence.frame?.url ?? null);
6128
- return page ? `${time} · ${page}` : time;
6129
- }
6130
6616
  function CalloutMark({ callout, imageSize, imageTop }) {
6131
6617
  const { outline, proposal } = callout;
6132
6618
  if (!outline) return null;
@@ -6142,7 +6628,8 @@ function CalloutMark({ callout, imageSize, imageTop }) {
6142
6628
  }
6143
6629
  });
6144
6630
  }
6145
- function ScreenFigure({ context, screen }) {
6631
+ function ScreenFigure({ context, newPage, screen }) {
6632
+ const atMs = screen.evidence.capturedAtMs ?? screen.evidence.atMs;
6146
6633
  const { callouts, image } = screen;
6147
6634
  const { canvasRef, measured, pillRef } = useMeasuredPills(callouts.map(({ label, proposal }) => `${proposal.id}:${label}`).join("|"));
6148
6635
  const width = measured?.width ?? 0;
@@ -6162,7 +6649,10 @@ function ScreenFigure({ context, screen }) {
6162
6649
  const belowImage = layout ? layout.height - imageTop - imageSize.height : 0;
6163
6650
  return /* @__PURE__ */ jsxs("figure", {
6164
6651
  className: "dt-ses-report",
6165
- children: [/* @__PURE__ */ jsxs("div", {
6652
+ children: [/* @__PURE__ */ jsx("figcaption", { children: /* @__PURE__ */ jsx(ShotCaption, {
6653
+ atMs,
6654
+ children: newPage
6655
+ }) }), /* @__PURE__ */ jsxs("div", {
6166
6656
  className: "dt-ses-report-canvas",
6167
6657
  "data-measured": layout !== null,
6168
6658
  ref: canvasRef,
@@ -6172,7 +6662,7 @@ function ScreenFigure({ context, screen }) {
6172
6662
  },
6173
6663
  children: [
6174
6664
  /* @__PURE__ */ jsx("img", {
6175
- alt: `Screenshot at ${screenCaption(screen)}`,
6665
+ alt: `Screenshot at ${formatClock(atMs)}`,
6176
6666
  height: image.height,
6177
6667
  src: image.url,
6178
6668
  width: image.width
@@ -6220,15 +6710,17 @@ function ScreenFigure({ context, screen }) {
6220
6710
  })
6221
6711
  })
6222
6712
  ]
6223
- }), /* @__PURE__ */ jsx("figcaption", {
6224
- className: "dt-meta",
6225
- children: screenCaption(screen)
6226
6713
  })]
6227
6714
  });
6228
6715
  }
6229
6716
  function ScreenReport({ context, report }) {
6230
- return /* @__PURE__ */ jsxs(Fragment, { children: [report.screens.map((screen) => /* @__PURE__ */ jsx(ScreenFigure, {
6717
+ const figures = report.screens.map((screen) => ({
6718
+ page: pagePath(screen.evidence.frame?.url ?? null),
6719
+ screen
6720
+ }));
6721
+ return /* @__PURE__ */ jsxs(Fragment, { children: [figures.map(({ page, screen }, index) => /* @__PURE__ */ jsx(ScreenFigure, {
6231
6722
  context,
6723
+ newPage: figures[index - 1]?.page === page ? null : page,
6232
6724
  screen
6233
6725
  }, screen.evidence.id)), report.unplaced.length > 0 && /* @__PURE__ */ jsxs("section", {
6234
6726
  className: "dt-ses-block",
@@ -6439,141 +6931,6 @@ function StartSession({ onStarted, onWriteNote }) {
6439
6931
  ] });
6440
6932
  }
6441
6933
  //#endregion
6442
- //#region src/sessions/ui/review/optimistic-change.ts
6443
- function useOptimisticChange(review) {
6444
- const run = useSessionAction();
6445
- const applyChange = async (change) => {
6446
- review.patch(change.show);
6447
- const saved = await run(change.failureLabel, change.request, () => {
6448
- applyChange(change);
6449
- });
6450
- if (saved === null) {
6451
- review.patch(change.rollback);
6452
- return null;
6453
- }
6454
- review.patch((detail) => change.settle(detail, saved));
6455
- return saved;
6456
- };
6457
- return applyChange;
6458
- }
6459
- //#endregion
6460
- //#region src/sessions/ui/review/proposals/use-proposal-actions.ts
6461
- const NOTICE_NEED_LENGTH = 60;
6462
- const DECISION_NOTICE = {
6463
- dismissed: "Dismissed",
6464
- kept: "Confirmed",
6465
- later: "Saved for later",
6466
- pending: "Back to undecided"
6467
- };
6468
- function quotedNeed(need) {
6469
- return `“${need.length > NOTICE_NEED_LENGTH ? `${need.slice(0, 59)}…` : need}”`;
6470
- }
6471
- function useProposalSave(review) {
6472
- const { api } = useSessionWorkspace();
6473
- const run = useSessionAction();
6474
- return async (failureLabel, request) => {
6475
- const proposal = await run(failureLabel, () => request(api));
6476
- if (proposal) review.patch((detail) => withProposal(detail, proposal));
6477
- return proposal;
6478
- };
6479
- }
6480
- function withDecision(proposalId, decision) {
6481
- return (detail) => ({
6482
- ...detail,
6483
- proposals: detail.proposals.map((item) => item.id === proposalId ? {
6484
- ...item,
6485
- decision
6486
- } : item)
6487
- });
6488
- }
6489
- function useDecide(review) {
6490
- const { api } = useSessionWorkspace();
6491
- const applyChange = useOptimisticChange(review);
6492
- const changeDecision = (options) => applyChange({
6493
- failureLabel: options.failureLabel,
6494
- request: () => api.decideProposal({
6495
- decision: options.to,
6496
- proposalId: options.proposalId
6497
- }),
6498
- rollback: withDecision(options.proposalId, options.from),
6499
- settle: withProposal,
6500
- show: withDecision(options.proposalId, options.to)
6501
- });
6502
- return async (proposal, decision) => {
6503
- const previous = proposal.decision;
6504
- if (!await changeDecision({
6505
- failureLabel: "The decision was not saved",
6506
- from: previous,
6507
- proposalId: proposal.id,
6508
- to: decision
6509
- })) return;
6510
- announce(`${DECISION_NOTICE[decision]} ${quotedNeed(proposal.need)}`, {
6511
- label: "Undo",
6512
- run: async () => {
6513
- if (await changeDecision({
6514
- failureLabel: "The decision was not undone",
6515
- from: decision,
6516
- proposalId: proposal.id,
6517
- to: previous
6518
- })) announce("Decision undone");
6519
- }
6520
- });
6521
- };
6522
- }
6523
- function withFeedbackIds(feedbackIds) {
6524
- return (detail) => ({
6525
- ...detail,
6526
- proposals: detail.proposals.map((item) => {
6527
- const feedbackId = feedbackIds.get(item.id);
6528
- return feedbackId ? {
6529
- ...item,
6530
- feedbackId
6531
- } : item;
6532
- })
6533
- });
6534
- }
6535
- function useAddToBoard(review) {
6536
- const { api } = useSessionWorkspace();
6537
- const run = useSessionAction();
6538
- return async (proposals) => {
6539
- const created = await Promise.all(proposals.map(async (proposal) => {
6540
- const result = await run(`${quotedNeed(proposal.need)} was not added to the board`, () => api.createFeedback(proposal.id));
6541
- return result ? [proposal.id, result.feedbackId] : null;
6542
- }));
6543
- const added = new Map(created.flatMap((entry) => entry ? [entry] : []));
6544
- if (added.size === 0) return;
6545
- review.patch(withFeedbackIds(added));
6546
- const onlyAdded = added.size === 1 ? proposals.find((proposal) => added.has(proposal.id)) : void 0;
6547
- announce(onlyAdded ? `Added ${quotedNeed(onlyAdded.need)} to the board` : `Added ${countLabel(added.size, "intention")} to the board`);
6548
- };
6549
- }
6550
- function useProposalActions(review) {
6551
- const save = useProposalSave(review);
6552
- const decide = useDecide(review);
6553
- return {
6554
- addToBoard: useAddToBoard(review),
6555
- clarify: (proposal, text) => save("The precision was not sent", (client) => client.clarifyProposal({
6556
- expectedRevision: proposal.revision,
6557
- proposalId: proposal.id,
6558
- text
6559
- })),
6560
- decide,
6561
- merge: async (targetId, sourceIds) => {
6562
- if (await save("The intentions were not merged", (client) => client.mergeProposals({
6563
- sourceIds,
6564
- targetId
6565
- }))) review.reload();
6566
- },
6567
- save,
6568
- separate: (proposalId) => save("The suggestion was not cleared", (client) => client.separateProposal(proposalId)),
6569
- verify: (proposalId, { note, status }) => save("The check was not saved", (client) => client.verifyProposal({
6570
- note: note || void 0,
6571
- proposalId,
6572
- status
6573
- }))
6574
- };
6575
- }
6576
- //#endregion
6577
6934
  //#region src/sessions/ui/review/proposals/delete-intention.tsx
6578
6935
  const DELETE_INTENTION_COPY = "Delete this intention? What later sessions added to it goes too, and so do the briefs Reflet saved that include it. Briefs you already copied or downloaded keep their text.";
6579
6936
  function DeleteConfirmation$1({ context, onCancel, proposal }) {
@@ -6884,32 +7241,26 @@ function IntentionSource({ context, proposal }) {
6884
7241
  if (!segment) return null;
6885
7242
  const [firstContext] = segment.context;
6886
7243
  const range = `${formatClock(segment.startMs)}–${formatClock(segment.endMs)}`;
6887
- return /* @__PURE__ */ jsxs("section", {
6888
- className: "dt-ses-block",
6889
- children: [
6890
- /* @__PURE__ */ jsxs("p", {
6891
- className: "dt-ses-source",
6892
- children: [
6893
- /* @__PURE__ */ jsx("span", {
6894
- className: "dt-ses-time",
6895
- children: range
6896
- }),
6897
- " “",
6898
- segment.text,
6899
- "”"
6900
- ]
6901
- }),
6902
- firstContext && /* @__PURE__ */ jsx("p", {
6903
- className: "dt-meta",
6904
- children: contextLine$1(firstContext)
6905
- }),
6906
- /* @__PURE__ */ jsx("button", {
6907
- className: "dt-ses-link",
6908
- onClick: (event) => context.navigation.onShowMoment(segment.id, event.currentTarget),
6909
- type: "button",
6910
- children: "Show context"
6911
- })
6912
- ]
7244
+ return /* @__PURE__ */ jsxs("button", {
7245
+ className: "dt-ses-source-btn",
7246
+ onClick: (event) => context.navigation.onShowMoment(segment.id, event.currentTarget),
7247
+ title: "Show the moment",
7248
+ type: "button",
7249
+ children: [/* @__PURE__ */ jsxs("span", {
7250
+ className: "dt-ses-source",
7251
+ children: [
7252
+ /* @__PURE__ */ jsx("span", {
7253
+ className: "dt-ses-time",
7254
+ children: range
7255
+ }),
7256
+ " “",
7257
+ segment.text,
7258
+ "”"
7259
+ ]
7260
+ }), firstContext && /* @__PURE__ */ jsx("span", {
7261
+ className: "dt-meta",
7262
+ children: contextLine$1(firstContext)
7263
+ })]
6913
7264
  });
6914
7265
  }
6915
7266
  //#endregion
@@ -7156,29 +7507,25 @@ function MergeSuggestion({ context, proposal }) {
7156
7507
  });
7157
7508
  }
7158
7509
  function DecisionButtons({ context, onEdit, proposal }) {
7159
- const decide = (decision) => () => {
7160
- if (proposal.decision !== decision) context.actions.decide(proposal, decision);
7161
- };
7510
+ const toggle = (decision) => decisionToggle(context.actions, proposal, decision);
7162
7511
  const isKept = proposal.decision === "kept";
7163
7512
  return /* @__PURE__ */ jsxs("div", {
7164
7513
  className: "dt-actions",
7165
7514
  children: [
7166
7515
  /* @__PURE__ */ jsxs("button", {
7167
- "aria-disabled": isKept,
7168
7516
  "aria-pressed": isKept,
7169
7517
  className: "dt-btn",
7170
7518
  "data-variant": "primary",
7171
- onClick: decide("kept"),
7519
+ onClick: toggle("kept"),
7172
7520
  type: "button",
7173
7521
  children: [/* @__PURE__ */ jsx(CheckIcon, { size: 14 }), "Confirm"]
7174
7522
  }),
7175
7523
  ICON_DECISIONS.map(({ decision, icon, label }) => /* @__PURE__ */ jsx("button", {
7176
- "aria-disabled": proposal.decision === decision,
7177
7524
  "aria-label": label,
7178
7525
  "aria-pressed": proposal.decision === decision,
7179
7526
  className: "dt-btn",
7180
7527
  "data-icon-only": true,
7181
- onClick: decide(decision),
7528
+ onClick: toggle(decision),
7182
7529
  title: label,
7183
7530
  type: "button",
7184
7531
  children: icon
@@ -7356,146 +7703,100 @@ function ProposalCard({ context, proposal }) {
7356
7703
  //#endregion
7357
7704
  //#region src/sessions/ui/review/proposals/proposals-tab.tsx
7358
7705
  function emptyCopy(detail) {
7359
- const { session } = detail;
7360
- if (session.collection !== "ended") return "Intentions appear here once the recording has uploaded and Reflet has read it.";
7706
+ if (detail.session.collection !== "ended") return "Intentions appear here once the recording has uploaded and Reflet has read it.";
7361
7707
  if (detail.segments.length === 0 && detail.marks.length === 0) return "Nothing was said or marked in this session.";
7362
- if (isAnalysisBusy(session)) return "Intentions appear here once Reflet has read the session.";
7363
- if (session.analysis.state === "failed") return "The transcript is kept. Read it, or write the intention yourself.";
7364
- if (session.analysis.state === "succeeded") return "No request stood out in this session. Read the transcript or write the intention yourself.";
7365
- return "Intentions appear here once the session is analyzed.";
7366
- }
7367
- function useAddProposal(context, onDone) {
7368
- const [need, setNeed] = useState("");
7369
- const [observed, setObserved] = useState("");
7370
- const [clientKey] = useState(() => crypto.randomUUID());
7371
- const [isSaving, setIsSaving] = useState(false);
7372
- const sessionId = context.detail.session.id;
7373
- const add = async () => {
7374
- setIsSaving(true);
7375
- try {
7376
- if (await context.actions.save("The intention was not added", (api) => api.createProposal({
7377
- clientKey,
7378
- need: need.trim(),
7379
- observed: observed.trim() || void 0,
7380
- sessionId
7381
- }))) onDone();
7382
- } finally {
7383
- setIsSaving(false);
7384
- }
7385
- };
7386
- return {
7387
- add,
7388
- isSaving,
7389
- need,
7390
- observed,
7391
- setNeed,
7392
- setObserved
7393
- };
7708
+ return "No request stood out in this session.";
7394
7709
  }
7395
- function AddProposalForm({ context, onDone }) {
7396
- const { add, isSaving, need, observed, setNeed, setObserved } = useAddProposal(context, onDone);
7397
- return /* @__PURE__ */ jsxs("form", {
7398
- className: "dt-card dt-ses-form",
7399
- onSubmit: (event) => {
7400
- event.preventDefault();
7401
- add();
7402
- },
7403
- children: [
7404
- /* @__PURE__ */ jsx(TextField, {
7405
- label: "Need",
7406
- maxLength: 300,
7407
- onChange: setNeed,
7408
- rows: 2,
7409
- value: need
7410
- }),
7411
- /* @__PURE__ */ jsx(TextField, {
7412
- label: "What you observed (optional)",
7413
- maxLength: 2e3,
7414
- onChange: setObserved,
7415
- rows: 3,
7416
- value: observed
7417
- }),
7418
- /* @__PURE__ */ jsx(FormActions, {
7419
- isSubmitDisabled: isSaving || need.trim() === "",
7420
- onCancel: onDone,
7421
- submitLabel: "Add intention"
7422
- })
7423
- ]
7710
+ function ConfirmAll({ context, undecided }) {
7711
+ return /* @__PURE__ */ jsx("div", {
7712
+ className: "dt-actions",
7713
+ children: /* @__PURE__ */ jsxs("button", {
7714
+ className: "dt-btn",
7715
+ onClick: () => context.actions.decide(undecided, "kept"),
7716
+ type: "button",
7717
+ children: [
7718
+ /* @__PURE__ */ jsx(CheckIcon, { size: 14 }),
7719
+ "Confirm all",
7720
+ /* @__PURE__ */ jsx("span", {
7721
+ className: "dt-btn-count",
7722
+ children: undecided.length
7723
+ })
7724
+ ]
7725
+ })
7424
7726
  });
7425
7727
  }
7426
7728
  function ProposalsTab({ context, view }) {
7427
- const [isAdding, setIsAdding] = useState(false);
7428
- const proposals = context.detail.proposals.filter((proposal) => proposal.mergedIntoId === null);
7429
- const { continuation } = context.detail;
7430
- const report = calloutReport(context.detail);
7729
+ const { detail, review } = context;
7730
+ const steps = useProcessingSteps(detail);
7731
+ const isProcessing = steps.some((step) => step.state !== "done");
7732
+ const proposals = detail.proposals.filter((proposal) => proposal.mergedIntoId === null);
7733
+ const undecided = proposals.filter((proposal) => proposal.decision === "pending" && proposal.attachment === null);
7734
+ const { continuation } = detail;
7735
+ const hasNothingToReview = proposals.length === 0 && !continuation;
7736
+ const report = calloutReport(detail);
7431
7737
  const showsScreens = report.screens.length > 0 && view === "screens";
7432
7738
  return /* @__PURE__ */ jsxs(Fragment, { children: [
7739
+ /* @__PURE__ */ jsx(ProcessingProgress, {
7740
+ detail,
7741
+ fillsPanel: hasNothingToReview,
7742
+ isProcessing,
7743
+ review,
7744
+ steps
7745
+ }),
7433
7746
  continuation && /* @__PURE__ */ jsx(UpdateCard, {
7434
7747
  context,
7435
7748
  continuation
7436
7749
  }),
7437
- proposals.length === 0 && !continuation && /* @__PURE__ */ jsx("p", {
7750
+ hasNothingToReview && !isProcessing && /* @__PURE__ */ jsx("p", {
7438
7751
  className: "dt-empty",
7439
- children: emptyCopy(context.detail)
7752
+ children: emptyCopy(detail)
7440
7753
  }),
7441
- showsScreens && /* @__PURE__ */ jsx(ScreenReport, {
7754
+ context.canEdit && undecided.length > 1 && /* @__PURE__ */ jsx(ConfirmAll, {
7442
7755
  context,
7443
- report
7756
+ undecided
7444
7757
  }),
7445
- proposals.filter((proposal) => !showsScreens || proposal.attachment !== null).map((proposal) => /* @__PURE__ */ jsx(ProposalCard, {
7446
- context,
7447
- proposal
7448
- }, proposal.id)),
7449
- context.canEdit && (isAdding ? /* @__PURE__ */ jsx(AddProposalForm, {
7758
+ showsScreens && /* @__PURE__ */ jsx(ScreenReport, {
7450
7759
  context,
7451
- onDone: () => setIsAdding(false)
7452
- }) : /* @__PURE__ */ jsx("div", {
7453
- className: "dt-actions",
7454
- children: /* @__PURE__ */ jsx("button", {
7455
- className: "dt-btn",
7456
- onClick: () => setIsAdding(true),
7457
- type: "button",
7458
- children: "Add an intention"
7459
- })
7460
- }))
7760
+ report
7761
+ }),
7762
+ proposals.filter((proposal) => !showsScreens || proposal.attachment !== null).map((proposal) => /* @__PURE__ */ jsx(ProposalCard, {
7763
+ context,
7764
+ proposal
7765
+ }, proposal.id))
7461
7766
  ] });
7462
7767
  }
7463
7768
  //#endregion
7464
7769
  //#region src/sessions/ui/review/review-footer.tsx
7770
+ const NOTHING_CONFIRMED = "Confirm an intention first";
7465
7771
  function ReviewFooter({ context, onOpenBrief }) {
7466
7772
  const confirmed = keptProposals(context.detail);
7467
7773
  const offBoard = confirmed.filter((proposal) => proposal.feedbackId === null);
7774
+ const hasConfirmed = confirmed.length > 0;
7775
+ const boardBlocker = hasConfirmed ? "Every confirmed intention is on the board" : NOTHING_CONFIRMED;
7468
7776
  return /* @__PURE__ */ jsxs("div", {
7469
- className: "dt-sheet-foot",
7470
- children: [
7471
- /* @__PURE__ */ jsxs("button", {
7472
- "aria-describedby": confirmed.length === 0 ? "rf-brief-hint" : void 0,
7473
- className: "dt-btn",
7474
- "data-variant": "primary",
7475
- disabled: confirmed.length === 0,
7476
- onClick: onOpenBrief,
7477
- type: "button",
7478
- children: ["Prepare brief", confirmed.length > 0 && /* @__PURE__ */ jsx("span", {
7479
- className: "dt-btn-count",
7480
- children: confirmed.length
7481
- })]
7482
- }),
7483
- offBoard.length > 0 && /* @__PURE__ */ jsxs("button", {
7484
- className: "dt-btn",
7485
- onClick: () => context.actions.addToBoard(offBoard),
7486
- type: "button",
7487
- children: [
7488
- "Add ",
7489
- offBoard.length,
7490
- " to board"
7491
- ]
7492
- }),
7493
- confirmed.length === 0 && /* @__PURE__ */ jsx("span", {
7494
- className: "dt-status",
7495
- id: "rf-brief-hint",
7496
- children: "Confirm an intention to brief your agent or add it to the board"
7497
- })
7498
- ]
7777
+ className: "dt-sheet-foot dt-ses-foot",
7778
+ children: [/* @__PURE__ */ jsxs("button", {
7779
+ className: "dt-btn",
7780
+ "data-variant": "primary",
7781
+ disabled: !hasConfirmed,
7782
+ onClick: onOpenBrief,
7783
+ title: hasConfirmed ? void 0 : NOTHING_CONFIRMED,
7784
+ type: "button",
7785
+ children: ["Prepare brief", hasConfirmed && /* @__PURE__ */ jsx("span", {
7786
+ className: "dt-btn-count",
7787
+ children: confirmed.length
7788
+ })]
7789
+ }), /* @__PURE__ */ jsxs("button", {
7790
+ className: "dt-btn",
7791
+ disabled: offBoard.length === 0,
7792
+ onClick: () => context.actions.addToBoard(offBoard),
7793
+ title: offBoard.length === 0 ? boardBlocker : void 0,
7794
+ type: "button",
7795
+ children: ["Add to board", offBoard.length > 0 && /* @__PURE__ */ jsx("span", {
7796
+ className: "dt-btn-count",
7797
+ children: offBoard.length
7798
+ })]
7799
+ })]
7499
7800
  });
7500
7801
  }
7501
7802
  //#endregion
@@ -7750,295 +8051,144 @@ function DeleteConfirmation({ onCancel, onDeleted, sessionId }) {
7750
8051
  })]
7751
8052
  });
7752
8053
  }
7753
- function ReviewHeader({ onDeleted, review, session }) {
7754
- const [isRenaming, setIsRenaming] = useState(false);
7755
- const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
7756
- const update = useSessionUpdate(review, session);
7757
- const isShared = session.visibility === "team";
7758
- const menuItems = [
7759
- {
7760
- label: "Rename",
7761
- onSelect: () => setIsRenaming(true)
7762
- },
7763
- {
7764
- label: isShared ? "Make private" : "Share with team",
7765
- onSelect: () => update("The sharing was not changed", { visibility: isShared ? "private" : "team" })
7766
- },
7767
- {
7768
- label: "Delete session",
7769
- onSelect: () => setIsConfirmingDelete(true),
7770
- variant: "danger"
7771
- }
7772
- ];
7773
- return /* @__PURE__ */ jsxs("header", {
7774
- className: "dt-ses-head",
7775
- children: [
7776
- isRenaming ? /* @__PURE__ */ jsx(TitleEditor, {
7777
- onDone: () => setIsRenaming(false),
7778
- review,
7779
- session
7780
- }) : /* @__PURE__ */ jsxs("div", {
7781
- className: "dt-card-head",
7782
- children: [/* @__PURE__ */ jsx("h2", {
7783
- className: "dt-ses-title",
7784
- tabIndex: -1,
7785
- children: session.title
7786
- }), session.canEdit && /* @__PURE__ */ jsx(Menu, {
7787
- items: menuItems,
7788
- trigger: {
7789
- className: "icon-btn",
7790
- content: /* @__PURE__ */ jsx(MoreIcon, {}),
7791
- label: "Session options"
7792
- }
7793
- })]
7794
- }),
7795
- /* @__PURE__ */ jsxs("p", {
7796
- className: "dt-meta",
7797
- children: [
7798
- formatDate(session.startedAt),
7799
- " · ",
7800
- shortDuration(session.durationMs),
7801
- " ·",
7802
- " ",
7803
- isShared ? "Shared with team" : "Private",
7804
- session.isMine ? "" : ` · ${session.authorName ?? "A teammate"}`
7805
- ]
7806
- }),
7807
- isConfirmingDelete && session.canEdit && /* @__PURE__ */ jsx(DeleteConfirmation, {
7808
- onCancel: () => setIsConfirmingDelete(false),
7809
- onDeleted,
7810
- sessionId: session.id
7811
- })
7812
- ]
7813
- });
7814
- }
7815
- //#endregion
7816
- //#region src/sessions/ui/review/review-tabs.tsx
7817
- const INTENTION_VIEWS = [{
7818
- label: "On screen",
7819
- value: "screens"
7820
- }, {
7821
- label: "Details",
7822
- value: "cards"
7823
- }];
7824
- function IntentionViewMenu({ place }) {
7825
- return /* @__PURE__ */ jsx(Menu, {
7826
- items: INTENTION_VIEWS.map((option) => ({
7827
- checked: option.value === place.intentionView,
7828
- label: option.label,
7829
- onSelect: () => place.selectIntentionView(option.value)
7830
- })),
7831
- trigger: {
7832
- className: "icon-btn",
7833
- content: /* @__PURE__ */ jsx(ViewOptionsIcon, {}),
7834
- label: "Show intentions"
7835
- }
7836
- });
7837
- }
7838
- function tabOptions(detail) {
7839
- return [
7840
- {
7841
- count: detail.proposals.filter((proposal) => proposal.mergedIntoId === null && proposal.attachment === null).length,
7842
- icon: /* @__PURE__ */ jsx(IntentionIcon, {}),
7843
- label: "Intentions",
7844
- value: "proposals"
7845
- },
7846
- {
7847
- count: null,
7848
- icon: /* @__PURE__ */ jsx(TranscriptIcon, {}),
7849
- label: "Transcript",
7850
- value: "transcript"
7851
- },
7852
- {
7853
- count: detail.evidence.length,
7854
- icon: /* @__PURE__ */ jsx(ImageIcon, {}),
7855
- label: "Evidence",
7856
- value: "evidence"
7857
- }
7858
- ];
7859
- }
7860
- function ReviewTabs({ detail, place }) {
7861
- const canPickView = place.tab === "proposals" && calloutReport(detail).screens.length > 0;
7862
- return /* @__PURE__ */ jsxs("div", {
7863
- className: "dt-toolbar",
7864
- children: [/* @__PURE__ */ jsx("div", {
7865
- className: "dt-tabs",
7866
- role: "tablist",
7867
- children: tabOptions(detail).map((option) => /* @__PURE__ */ jsxs("button", {
7868
- "aria-selected": option.value === place.tab,
7869
- className: "dt-tab",
7870
- onClick: () => place.selectTab(option.value),
7871
- role: "tab",
7872
- type: "button",
7873
- children: [
7874
- option.icon,
7875
- option.label,
7876
- option.count !== null && /* @__PURE__ */ jsx("span", {
7877
- className: "dt-tab-count",
7878
- children: option.count
7879
- })
7880
- ]
7881
- }, option.value))
7882
- }), canPickView && /* @__PURE__ */ jsx(IntentionViewMenu, { place })]
7883
- });
7884
- }
7885
- //#endregion
7886
- //#region src/sessions/ui/review/processing-steps.ts
7887
- function uploadStep(pendingUploads) {
7888
- return pendingUploads > 0 ? {
7889
- analyzeLabel: null,
7890
- key: "upload",
7891
- label: `Uploading · ${pendingUploads} waiting`,
7892
- state: "active"
7893
- } : {
7894
- analyzeLabel: null,
7895
- key: "upload",
7896
- label: "Uploaded",
7897
- state: "done"
7898
- };
7899
- }
7900
- function transcriptionStep(detail, isAnalysisQueued) {
7901
- const voice = detail.segments.filter((segment) => segment.source === "voice");
7902
- const transcribing = voice.filter(({ transcription }) => transcription.state === "pending" || transcription.state === "running").length;
7903
- const step = {
7904
- analyzeLabel: null,
7905
- key: "transcription"
7906
- };
7907
- if (transcribing > 0) {
7908
- const transcribed = voice.length - transcribing;
7909
- return {
7910
- ...step,
7911
- label: `Transcribing your voice · ${transcribed} of ${voice.length}`,
7912
- state: "active"
7913
- };
7914
- }
7915
- if (voice.length > 0) return {
7916
- ...step,
7917
- label: "Voice transcribed",
7918
- state: "done"
7919
- };
7920
- return isAnalysisQueued ? {
7921
- ...step,
7922
- label: "Transcribe your voice",
7923
- state: "waiting"
7924
- } : null;
7925
- }
7926
- function comparedRevision(detail) {
7927
- const { continuation } = detail;
7928
- return continuation?.intention && continuation.contribution.detachedAt === null ? continuation.contribution.baseRevision : null;
7929
- }
7930
- function intentionStep(detail, isAnalysisQueued) {
7931
- const { session } = detail;
7932
- const { analysis } = session;
7933
- const revision = comparedRevision(detail);
7934
- const isComparing = revision !== null;
7935
- const step = {
7936
- analyzeLabel: null,
7937
- key: "intentions"
7938
- };
7939
- if (analysis.state === "running") {
7940
- const label = isComparing ? `Comparing with revision ${revision}…` : "Finding intentions with AI…";
7941
- return {
7942
- ...step,
7943
- label,
7944
- state: "active"
7945
- };
7946
- }
7947
- if (analysis.state === "waiting" || isAnalysisQueued) {
7948
- const label = isComparing ? `Compare with revision ${revision}` : "Find intentions with AI";
7949
- return {
7950
- ...step,
7951
- label,
7952
- state: "waiting"
7953
- };
7954
- }
7955
- if (analysis.state === "failed") {
7956
- const failure = isComparing ? "The comparison failed" : "Analysis failed";
7957
- const reason = analysis.error ?? "Unknown error.";
7958
- return {
7959
- ...step,
7960
- analyzeLabel: "Try again",
7961
- label: `${failure}: ${reason}`,
7962
- state: "failed"
7963
- };
7964
- }
7965
- if (isAnalysisOutdated(session)) return {
7966
- ...step,
7967
- analyzeLabel: "Analyze again",
7968
- label: "The transcript changed since the analysis.",
7969
- state: "waiting"
7970
- };
7971
- if (analysis.state === "idle") return detail.segments.length > 0 ? {
7972
- ...step,
7973
- analyzeLabel: "Analyze",
7974
- label: "Not analyzed yet.",
7975
- state: "waiting"
7976
- } : null;
7977
- const label = isComparing ? `Compared with revision ${revision}` : `Found ${countLabel(analysis.proposalCount ?? 0, "intention")}`;
7978
- return {
7979
- ...step,
7980
- label,
7981
- state: "done"
7982
- };
7983
- }
7984
- function processingSteps(detail, pendingUploads) {
7985
- if (detail.session.collection !== "ended") return [];
7986
- const isAnalysisQueued = pendingUploads > 0 && detail.session.analysis.state === "idle";
7987
- return [
7988
- detail.session.isMine ? uploadStep(pendingUploads) : null,
7989
- transcriptionStep(detail, isAnalysisQueued),
7990
- intentionStep(detail, isAnalysisQueued)
7991
- ].filter((step) => step !== null);
7992
- }
7993
- //#endregion
7994
- //#region src/sessions/ui/review/status-strip.tsx
7995
- function AnalyzeButton({ detail, label, review }) {
7996
- const { api } = useSessionWorkspace();
7997
- const run = useSessionAction();
7998
- const analyze = async () => {
7999
- const status = await run("The analysis did not start", () => api.analyze(detail.session.id));
8000
- if (status) review.patch((current) => ({
8001
- ...current,
8002
- session: {
8003
- ...current.session,
8004
- analysis: status
8005
- }
8006
- }));
8007
- };
8008
- return /* @__PURE__ */ jsx("button", {
8009
- className: "dt-btn",
8010
- onClick: analyze,
8011
- type: "button",
8012
- children: label
8054
+ function SessionFacts({ session }) {
8055
+ const isShared = session.visibility === "team";
8056
+ const sharing = isShared ? "Shared with team" : "Private";
8057
+ return /* @__PURE__ */ jsxs("p", {
8058
+ className: "dt-ses-facts",
8059
+ children: [
8060
+ /* @__PURE__ */ jsx("span", { children: formatDate(session.startedAt) }),
8061
+ /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx(ClockIcon, {}), shortDuration(session.durationMs)] }),
8062
+ /* @__PURE__ */ jsx("span", {
8063
+ "aria-label": sharing,
8064
+ role: "img",
8065
+ title: sharing,
8066
+ children: isShared ? /* @__PURE__ */ jsx(TeamIcon, {}) : /* @__PURE__ */ jsx(LockIcon, {})
8067
+ }),
8068
+ !session.isMine && /* @__PURE__ */ jsx("span", { children: session.authorName ?? "A teammate" })
8069
+ ]
8013
8070
  });
8014
8071
  }
8015
- function StepRow({ detail, review, step }) {
8016
- return /* @__PURE__ */ jsxs("li", {
8017
- "data-state": step.state,
8018
- children: [/* @__PURE__ */ jsx("span", { children: step.label }), step.analyzeLabel && detail.session.canEdit && /* @__PURE__ */ jsx(AnalyzeButton, {
8019
- detail,
8020
- label: step.analyzeLabel,
8021
- review
8072
+ function ReviewHeader({ onDeleted, review, session }) {
8073
+ const [isRenaming, setIsRenaming] = useState(false);
8074
+ const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
8075
+ const update = useSessionUpdate(review, session);
8076
+ const isShared = session.visibility === "team";
8077
+ const menuItems = [
8078
+ {
8079
+ label: "Rename",
8080
+ onSelect: () => setIsRenaming(true)
8081
+ },
8082
+ {
8083
+ label: isShared ? "Make private" : "Share with team",
8084
+ onSelect: () => update("The sharing was not changed", { visibility: isShared ? "private" : "team" })
8085
+ },
8086
+ {
8087
+ label: "Delete session",
8088
+ onSelect: () => setIsConfirmingDelete(true),
8089
+ variant: "danger"
8090
+ }
8091
+ ];
8092
+ return /* @__PURE__ */ jsxs("header", {
8093
+ className: "dt-ses-head",
8094
+ children: [isRenaming ? /* @__PURE__ */ jsx(TitleEditor, {
8095
+ onDone: () => setIsRenaming(false),
8096
+ review,
8097
+ session
8098
+ }) : /* @__PURE__ */ jsxs("div", {
8099
+ className: "dt-card-head",
8100
+ children: [
8101
+ /* @__PURE__ */ jsx("h2", {
8102
+ className: "dt-ses-title",
8103
+ tabIndex: -1,
8104
+ title: session.title,
8105
+ children: session.title
8106
+ }),
8107
+ /* @__PURE__ */ jsx(SessionFacts, { session }),
8108
+ session.canEdit && /* @__PURE__ */ jsx(Menu, {
8109
+ items: menuItems,
8110
+ trigger: {
8111
+ className: "icon-btn",
8112
+ content: /* @__PURE__ */ jsx(MoreIcon, {}),
8113
+ label: "Session options"
8114
+ }
8115
+ })
8116
+ ]
8117
+ }), isConfirmingDelete && session.canEdit && /* @__PURE__ */ jsx(DeleteConfirmation, {
8118
+ onCancel: () => setIsConfirmingDelete(false),
8119
+ onDeleted,
8120
+ sessionId: session.id
8022
8121
  })]
8023
8122
  });
8024
8123
  }
8025
- function StatusStrip({ detail, review }) {
8026
- const { account } = useSessionWorkspace();
8027
- const sync = useSyncStatus(account.key);
8028
- const steps = processingSteps(detail, detail.session.isMine ? sync.pendingOperations : 0);
8029
- const isProcessed = steps.every((step) => step.state === "done");
8030
- return /* @__PURE__ */ jsx("div", {
8031
- "aria-live": "polite",
8032
- className: "dt-ses-strip",
8033
- children: !isProcessed && /* @__PURE__ */ jsx("ol", {
8034
- "aria-label": "Processing",
8035
- className: "dt-ses-steps",
8036
- children: steps.map((step) => /* @__PURE__ */ jsx(StepRow, {
8037
- detail,
8038
- review,
8039
- step
8040
- }, step.key))
8041
- })
8124
+ //#endregion
8125
+ //#region src/sessions/ui/review/review-tabs.tsx
8126
+ const INTENTION_VIEWS = [{
8127
+ label: "On screen",
8128
+ value: "screens"
8129
+ }, {
8130
+ label: "Details",
8131
+ value: "cards"
8132
+ }];
8133
+ function IntentionViewMenu({ place }) {
8134
+ return /* @__PURE__ */ jsx(Menu, {
8135
+ items: INTENTION_VIEWS.map((option) => ({
8136
+ checked: option.value === place.intentionView,
8137
+ label: option.label,
8138
+ onSelect: () => place.selectIntentionView(option.value)
8139
+ })),
8140
+ trigger: {
8141
+ className: "icon-btn",
8142
+ content: /* @__PURE__ */ jsx(ViewOptionsIcon, {}),
8143
+ label: "Show intentions"
8144
+ }
8145
+ });
8146
+ }
8147
+ function tabOptions(detail) {
8148
+ return [
8149
+ {
8150
+ count: detail.proposals.filter((proposal) => proposal.mergedIntoId === null && proposal.attachment === null).length,
8151
+ icon: /* @__PURE__ */ jsx(IntentionIcon, {}),
8152
+ label: "Intentions",
8153
+ value: "proposals"
8154
+ },
8155
+ {
8156
+ count: null,
8157
+ icon: /* @__PURE__ */ jsx(TranscriptIcon, {}),
8158
+ label: "Transcript",
8159
+ value: "transcript"
8160
+ },
8161
+ {
8162
+ count: detail.evidence.length,
8163
+ icon: /* @__PURE__ */ jsx(ImageIcon, {}),
8164
+ label: "Screenshots",
8165
+ value: "evidence"
8166
+ }
8167
+ ];
8168
+ }
8169
+ function ReviewTabs({ detail, place }) {
8170
+ const canPickView = place.tab === "proposals" && calloutReport(detail).screens.length > 0;
8171
+ return /* @__PURE__ */ jsxs("div", {
8172
+ className: "dt-toolbar",
8173
+ children: [/* @__PURE__ */ jsx("div", {
8174
+ className: "dt-tabs",
8175
+ role: "tablist",
8176
+ children: tabOptions(detail).map((option) => /* @__PURE__ */ jsxs("button", {
8177
+ "aria-selected": option.value === place.tab,
8178
+ className: "dt-tab",
8179
+ onClick: () => place.selectTab(option.value),
8180
+ role: "tab",
8181
+ type: "button",
8182
+ children: [
8183
+ option.icon,
8184
+ option.label,
8185
+ option.count !== null && /* @__PURE__ */ jsx("span", {
8186
+ className: "dt-tab-count",
8187
+ children: option.count
8188
+ })
8189
+ ]
8190
+ }, option.value))
8191
+ }), canPickView && /* @__PURE__ */ jsx(IntentionViewMenu, { place })]
8042
8192
  });
8043
8193
  }
8044
8194
  //#endregion
@@ -8190,17 +8340,16 @@ function LinkEvidenceItem({ link }) {
8190
8340
  ]
8191
8341
  });
8192
8342
  }
8193
- function MomentLink({ atMs, onShowMoment }) {
8194
- if (!onShowMoment) return null;
8195
- return /* @__PURE__ */ jsx("button", {
8196
- "aria-label": `Show the moment at ${formatClock(atMs)}`,
8197
- className: "dt-ses-link",
8198
- onClick: (event) => onShowMoment(event.currentTarget),
8199
- type: "button",
8200
- children: "Show moment"
8201
- });
8343
+ function EvidenceFace({ evidence }) {
8344
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ShotCaption, {
8345
+ atMs: evidence.atMs,
8346
+ children: evidenceCaption(evidence)
8347
+ }), /* @__PURE__ */ jsx(EvidenceThumb, {
8348
+ evidence,
8349
+ isLarge: true
8350
+ })] });
8202
8351
  }
8203
- function EvidenceItem({ evidence, openers, removal }) {
8352
+ function EvidenceItem({ evidence, onShowMoment, removal }) {
8204
8353
  const caption = `${formatClock(evidence.atMs)} · ${evidenceCaption(evidence)}`;
8205
8354
  if (evidence.link) return /* @__PURE__ */ jsx(LinkEvidenceItem, { link: evidence.link });
8206
8355
  if (!isViewable(evidence)) return /* @__PURE__ */ jsx(MissingEvidenceItem, {
@@ -8209,27 +8358,22 @@ function EvidenceItem({ evidence, openers, removal }) {
8209
8358
  isJustRemoved: evidence.id === removal.removedId
8210
8359
  });
8211
8360
  return /* @__PURE__ */ jsxs("li", {
8212
- className: "dt-card",
8361
+ className: "dt-card dt-ses-openable",
8213
8362
  children: [
8214
- /* @__PURE__ */ jsx("button", {
8215
- "aria-label": `Enlarge: ${caption}`,
8216
- className: "dt-ses-shot-btn",
8217
- onClick: openers.onEnlarge,
8363
+ onShowMoment ? /* @__PURE__ */ jsx("button", {
8364
+ "aria-label": `Show the moment at ${caption}`,
8365
+ className: "dt-ses-open dt-ses-shot-face",
8366
+ onClick: (event) => onShowMoment(event.currentTarget),
8218
8367
  type: "button",
8219
- children: /* @__PURE__ */ jsx(EvidenceThumb, { evidence })
8220
- }),
8221
- /* @__PURE__ */ jsx("p", {
8222
- className: "dt-meta",
8223
- children: caption
8368
+ children: /* @__PURE__ */ jsx(EvidenceFace, { evidence })
8369
+ }) : /* @__PURE__ */ jsx("div", {
8370
+ className: "dt-ses-shot-face",
8371
+ children: /* @__PURE__ */ jsx(EvidenceFace, { evidence })
8224
8372
  }),
8225
8373
  evidence.note && /* @__PURE__ */ jsx("p", {
8226
8374
  className: "dt-note-text",
8227
8375
  children: evidence.note
8228
8376
  }),
8229
- /* @__PURE__ */ jsx(MomentLink, {
8230
- atMs: evidence.atMs,
8231
- onShowMoment: openers.onShowMoment
8232
- }),
8233
8377
  removal.canRemove && /* @__PURE__ */ jsx(RemoveEvidence, {
8234
8378
  evidence,
8235
8379
  removal
@@ -8237,54 +8381,6 @@ function EvidenceItem({ evidence, openers, removal }) {
8237
8381
  ]
8238
8382
  });
8239
8383
  }
8240
- function EnlargedEvidence({ evidence, onBack, removal }) {
8241
- return /* @__PURE__ */ jsxs(Fragment, { children: [
8242
- /* @__PURE__ */ jsx("button", {
8243
- className: "dt-ses-link",
8244
- onClick: onBack,
8245
- type: "button",
8246
- children: "← All evidence"
8247
- }),
8248
- /* @__PURE__ */ jsx(EvidenceThumb, {
8249
- evidence,
8250
- isLarge: true
8251
- }),
8252
- /* @__PURE__ */ jsxs("p", {
8253
- className: "dt-meta",
8254
- children: [
8255
- formatClock(evidence.atMs),
8256
- " · ",
8257
- evidenceCaption(evidence)
8258
- ]
8259
- }),
8260
- removal.canRemove && /* @__PURE__ */ jsx(RemoveEvidence, {
8261
- evidence,
8262
- removal
8263
- })
8264
- ] });
8265
- }
8266
- function EvidenceList({ detail, openers, removal }) {
8267
- const { onEnlarge, onShowMoment } = openers;
8268
- const { evidence } = detail;
8269
- const momentOpener = (atMs) => {
8270
- const segmentId = nearestSegmentId(detail.segments, atMs);
8271
- return segmentId ? (opener) => onShowMoment(segmentId, opener) : null;
8272
- };
8273
- return /* @__PURE__ */ jsxs(Fragment, { children: [evidence.length === 0 && /* @__PURE__ */ jsx("p", {
8274
- className: "dt-empty",
8275
- children: "No screenshots or links in this session."
8276
- }), /* @__PURE__ */ jsx("ul", {
8277
- className: "dt-ses-rows",
8278
- children: evidence.map((item) => /* @__PURE__ */ jsx(EvidenceItem, {
8279
- evidence: item,
8280
- openers: {
8281
- onEnlarge: () => onEnlarge(item.id),
8282
- onShowMoment: momentOpener(item.atMs)
8283
- },
8284
- removal
8285
- }, item.id))
8286
- })] });
8287
- }
8288
8384
  function AttachLinkAction({ detail, review }) {
8289
8385
  const [isAttaching, setIsAttaching] = useState(false);
8290
8386
  if (isAttaching) return /* @__PURE__ */ jsx(AttachLink, {
@@ -8303,34 +8399,34 @@ function AttachLinkAction({ detail, review }) {
8303
8399
  });
8304
8400
  }
8305
8401
  function EvidenceTab({ detail, onShowMoment, review }) {
8306
- const [enlargedId, setEnlargedId] = useState(null);
8307
8402
  const [removedId, setRemovedId] = useState(null);
8308
- const enlarged = detail.evidence.find((item) => item.id === enlargedId && isViewable(item));
8403
+ const { canEdit } = detail.session;
8309
8404
  const removal = {
8310
- canRemove: detail.session.canEdit,
8311
- onRemoved: (evidenceId) => {
8312
- setEnlargedId(null);
8313
- setRemovedId(evidenceId);
8314
- },
8405
+ canRemove: canEdit,
8406
+ onRemoved: setRemovedId,
8315
8407
  removedId,
8316
8408
  review
8317
8409
  };
8318
- if (enlarged) return /* @__PURE__ */ jsx(EnlargedEvidence, {
8319
- evidence: enlarged,
8320
- onBack: () => setEnlargedId(null),
8321
- removal
8322
- });
8323
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(EvidenceList, {
8324
- detail,
8325
- openers: {
8326
- onEnlarge: setEnlargedId,
8327
- onShowMoment
8328
- },
8329
- removal
8330
- }), detail.session.canEdit && /* @__PURE__ */ jsx(AttachLinkAction, {
8410
+ const momentOpener = (atMs) => {
8411
+ const segmentId = nearestSegmentId(detail.segments, atMs);
8412
+ return segmentId ? (opener) => onShowMoment(segmentId, opener) : null;
8413
+ };
8414
+ const attachLink = canEdit && /* @__PURE__ */ jsx(AttachLinkAction, {
8331
8415
  detail,
8332
8416
  review
8333
- })] });
8417
+ });
8418
+ if (detail.evidence.length === 0) return /* @__PURE__ */ jsxs("div", {
8419
+ className: "dt-empty",
8420
+ children: [/* @__PURE__ */ jsx("p", { children: "No screenshots or links in this session." }), attachLink]
8421
+ });
8422
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
8423
+ className: "dt-ses-rows",
8424
+ children: detail.evidence.map((item) => /* @__PURE__ */ jsx(EvidenceItem, {
8425
+ evidence: item,
8426
+ onShowMoment: momentOpener(item.atMs),
8427
+ removal
8428
+ }, item.id))
8429
+ }), attachLink] });
8334
8430
  }
8335
8431
  //#endregion
8336
8432
  //#region src/sessions/ui/review/transcript/segment-row.tsx
@@ -8444,22 +8540,17 @@ function ContextChips({ segment }) {
8444
8540
  }, `${item.kind}:${item.atMs}:${item.label}`))]
8445
8541
  });
8446
8542
  }
8447
- function SegmentLinks({ onEdit, onShowMoment, time }) {
8448
- return /* @__PURE__ */ jsxs("div", {
8449
- className: "dt-actions",
8450
- children: [/* @__PURE__ */ jsx("button", {
8451
- "aria-label": `Show context at ${time}`,
8452
- className: "dt-ses-link",
8453
- onClick: (event) => onShowMoment(event.currentTarget),
8454
- type: "button",
8455
- children: "Show context"
8456
- }), onEdit && /* @__PURE__ */ jsx("button", {
8457
- "aria-label": `Edit passage at ${time}`,
8458
- className: "dt-ses-link",
8459
- onClick: onEdit,
8460
- type: "button",
8461
- children: "Edit"
8462
- })]
8543
+ function MomentTime({ onShowMoment, time }) {
8544
+ if (!onShowMoment) return /* @__PURE__ */ jsx("span", {
8545
+ className: "dt-ses-time",
8546
+ children: time
8547
+ });
8548
+ return /* @__PURE__ */ jsx("button", {
8549
+ "aria-label": `Show the moment at ${time}`,
8550
+ className: "dt-ses-time dt-ses-open",
8551
+ onClick: (event) => onShowMoment(event.currentTarget),
8552
+ type: "button",
8553
+ children: time
8463
8554
  });
8464
8555
  }
8465
8556
  function SegmentRow({ editing, onShowMoment, segment }) {
@@ -8468,14 +8559,15 @@ function SegmentRow({ editing, onShowMoment, segment }) {
8468
8559
  const time = formatClock(segment.startMs);
8469
8560
  const isSettled = ["done", "empty"].includes(segment.transcription.state);
8470
8561
  return /* @__PURE__ */ jsxs("li", {
8471
- className: "dt-ses-seg",
8472
- children: [/* @__PURE__ */ jsx("span", {
8473
- className: "dt-ses-time",
8474
- children: time
8475
- }), /* @__PURE__ */ jsxs("div", {
8476
- className: "dt-ses-seg-body",
8477
- children: [
8478
- isEditing ? /* @__PURE__ */ jsx(SegmentEditor, {
8562
+ className: "dt-ses-seg dt-ses-openable",
8563
+ children: [
8564
+ /* @__PURE__ */ jsx(MomentTime, {
8565
+ onShowMoment: isEditing ? null : onShowMoment,
8566
+ time
8567
+ }),
8568
+ /* @__PURE__ */ jsxs("div", {
8569
+ className: "dt-ses-seg-body",
8570
+ children: [isEditing ? /* @__PURE__ */ jsx(SegmentEditor, {
8479
8571
  onDone: () => setIsEditing(false),
8480
8572
  review,
8481
8573
  segment
@@ -8483,15 +8575,17 @@ function SegmentRow({ editing, onShowMoment, segment }) {
8483
8575
  onType: canEdit ? () => setIsEditing(true) : void 0,
8484
8576
  review,
8485
8577
  segment
8486
- }),
8487
- /* @__PURE__ */ jsx(ContextChips, { segment }),
8488
- /* @__PURE__ */ jsx(SegmentLinks, {
8489
- onEdit: canEdit && isSettled && !isEditing ? () => setIsEditing(true) : null,
8490
- onShowMoment,
8491
- time
8492
- })
8493
- ]
8494
- })]
8578
+ }), /* @__PURE__ */ jsx(ContextChips, { segment })]
8579
+ }),
8580
+ canEdit && isSettled && !isEditing && /* @__PURE__ */ jsx("button", {
8581
+ "aria-label": `Edit passage at ${time}`,
8582
+ className: "icon-btn glass dt-ses-hover-action",
8583
+ onClick: () => setIsEditing(true),
8584
+ title: "Edit",
8585
+ type: "button",
8586
+ children: /* @__PURE__ */ jsx(PencilIcon, {})
8587
+ })
8588
+ ]
8495
8589
  });
8496
8590
  }
8497
8591
  //#endregion
@@ -8573,25 +8667,18 @@ function transcriptForAgent(detail) {
8573
8667
  //#endregion
8574
8668
  //#region src/sessions/ui/review/transcript/transcript-tab.tsx
8575
8669
  function TimelineNote({ atMs, children, onShowMoment }) {
8576
- const time = formatClock(atMs);
8577
8670
  return /* @__PURE__ */ jsxs("li", {
8578
- className: "dt-ses-seg",
8671
+ className: "dt-ses-seg dt-ses-openable",
8579
8672
  "data-kind": "note",
8580
- children: [/* @__PURE__ */ jsx("span", {
8581
- className: "dt-ses-time",
8582
- children: time
8583
- }), /* @__PURE__ */ jsxs("div", {
8673
+ children: [/* @__PURE__ */ jsx(MomentTime, {
8674
+ onShowMoment,
8675
+ time: formatClock(atMs)
8676
+ }), /* @__PURE__ */ jsx("div", {
8584
8677
  className: "dt-ses-seg-body",
8585
- children: [/* @__PURE__ */ jsx("p", {
8678
+ children: /* @__PURE__ */ jsx("p", {
8586
8679
  className: "dt-meta",
8587
8680
  children
8588
- }), onShowMoment && /* @__PURE__ */ jsx("button", {
8589
- "aria-label": `Show the moment at ${time}`,
8590
- className: "dt-ses-link",
8591
- onClick: (event) => onShowMoment(event.currentTarget),
8592
- type: "button",
8593
- children: "Show moment"
8594
- })]
8681
+ })
8595
8682
  })]
8596
8683
  });
8597
8684
  }
@@ -8786,21 +8873,14 @@ function ReviewPanels({ context, place }) {
8786
8873
  }
8787
8874
  function ReviewTop({ context, links }) {
8788
8875
  const { detail, review } = context;
8789
- return /* @__PURE__ */ jsxs(Fragment, { children: [
8790
- /* @__PURE__ */ jsx(ReviewHeader, {
8791
- onDeleted: links.onDeleted,
8792
- review,
8793
- session: detail.session
8794
- }),
8795
- /* @__PURE__ */ jsx(StatusStrip, {
8796
- detail,
8797
- review
8798
- }),
8799
- detail.continuation && /* @__PURE__ */ jsx(ContinuationHeader, {
8800
- continuation: detail.continuation,
8801
- onOpenSession: links.onOpenSession
8802
- })
8803
- ] });
8876
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ReviewHeader, {
8877
+ onDeleted: links.onDeleted,
8878
+ review,
8879
+ session: detail.session
8880
+ }), detail.continuation && /* @__PURE__ */ jsx(ContinuationHeader, {
8881
+ continuation: detail.continuation,
8882
+ onOpenSession: links.onOpenSession
8883
+ })] });
8804
8884
  }
8805
8885
  function ReviewScreen({ detail, isBriefOpen, links, review }) {
8806
8886
  const place = useReviewPlace();
@@ -9169,7 +9249,8 @@ p.dt-ses-diff { display: block; }
9169
9249
  //#endregion
9170
9250
  //#region src/sessions/ui/review/screens/report-styles.ts
9171
9251
  const REPORT_STYLES = `
9172
- .dt-ses-report { display: flex; flex-direction: column; gap: 4px; width: 100%; min-width: 0; margin: 0; }
9252
+ .dt-ses-report { display: flex; flex-direction: column; gap: 6px; width: 100%; min-width: 0; margin: 0; }
9253
+ .dt-ses-report + .dt-ses-report { margin-top: 12px; }
9173
9254
  .dt-ses-report-canvas { position: relative; width: 100%; }
9174
9255
  .dt-ses-report-canvas img { display: block; width: 100%; max-width: 100%; height: auto; border-radius: 8px; box-shadow: var(--rf-shadow-inset); }
9175
9256
  .dt-ses-report-canvas[data-measured="false"] .dt-ses-callout { visibility: hidden; }
@@ -9183,21 +9264,25 @@ const REPORT_STYLES = `
9183
9264
  .dt-ses-callouts { margin: 0; padding: 0; list-style: none; }
9184
9265
  .dt-ses-callouts[data-flow="list"] { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
9185
9266
  .dt-ses-callout {
9186
- display: flex; align-items: center; gap: 4px; max-width: 100%; padding: 3px; border-radius: var(--rf-pill);
9187
- background: var(--rf-bg); box-shadow: var(--rf-shadow-inset), var(--rf-shadow-low); font-size: 12px; line-height: 1.3;
9267
+ --dt-callout-control: 22px; --dt-callout-inset: 3px;
9268
+ display: flex; align-items: flex-start; gap: 2px; max-width: 100%; padding: var(--dt-callout-inset);
9269
+ border-radius: calc(var(--dt-callout-control) / 2 + var(--dt-callout-inset));
9270
+ background: var(--rf-bg); box-shadow: var(--rf-shadow-inset), var(--rf-shadow-low); font-size: 12px; line-height: 16px;
9188
9271
  }
9189
9272
  .dt-ses-report-canvas .dt-ses-callout { position: absolute; top: 0; left: 0; width: max-content; max-width: min(280px, 100%); }
9190
9273
  .dt-ses-callout-number {
9191
- display: grid; place-items: center; flex: none; min-width: 20px; height: 20px; padding: 0 5px; border-radius: var(--rf-pill);
9192
- background: var(--rf-accent); color: var(--rf-accent-text, #fff); font-size: 11px; font-weight: 650; font-variant-numeric: tabular-nums;
9274
+ display: grid; place-items: center; flex: none; min-width: var(--dt-callout-control); height: var(--dt-callout-control); padding: 0 6px;
9275
+ border-radius: calc(var(--dt-callout-control) / 2); background: var(--rf-accent); color: var(--rf-accent-text, #fff);
9276
+ font-size: 11px; font-weight: 650; font-variant-numeric: tabular-nums;
9193
9277
  }
9194
9278
  .dt-ses-callout[data-decision="kept"] .dt-ses-callout-number { background: var(--rf-success); }
9195
9279
  .dt-ses-callout[data-decision="dismissed"] .dt-ses-callout-number { background: var(--rf-text-muted); }
9196
- .dt-ses-callout-label { min-width: 0; padding: 0 4px; font-weight: 550; text-align: left; overflow-wrap: anywhere; }
9280
+ .dt-ses-callout-label { flex: 1 1 auto; min-width: 0; padding: 3px 4px; font-weight: 550; text-align: left; overflow-wrap: anywhere; }
9197
9281
  .dt-ses-callout-label:hover { color: var(--rf-accent); }
9198
9282
  .dt-ses-callout[data-decision="dismissed"] .dt-ses-callout-label { color: var(--rf-text-muted); text-decoration: line-through; }
9199
9283
  .dt-ses-callout-btn {
9200
- display: grid; place-items: center; flex: none; width: 22px; height: 22px; border-radius: 50%; color: var(--rf-text-muted);
9284
+ display: grid; place-items: center; flex: none; width: var(--dt-callout-control); height: var(--dt-callout-control);
9285
+ border-radius: calc(var(--dt-callout-control) / 2); color: var(--rf-text-muted);
9201
9286
  transition: background-color 120ms var(--rf-ease), color 120ms var(--rf-ease);
9202
9287
  }
9203
9288
  .dt-ses-callout-btn svg { width: 13px; height: 13px; }
@@ -9239,21 +9324,47 @@ const SESSIONS_STYLES = `${`
9239
9324
  .dt-ses-list li + li { margin-top: 3px; }
9240
9325
  .dt-ses-link { align-self: flex-start; padding: 0; color: var(--rf-accent); font-size: 12px; text-align: left; }
9241
9326
  .dt-ses-link:hover { text-decoration: underline; }
9242
- .dt-ses-problem, .dt-ses-banner, .dt-ses-form, .dt-ses-head, .dt-ses-strip, .dt-ses-handoff { display: flex; flex-direction: column; gap: 6px; }
9327
+ .dt-ses-problem, .dt-ses-banner, .dt-ses-form, .dt-ses-head, .dt-ses-handoff { display: flex; flex-direction: column; gap: 6px; }
9243
9328
  .dt-ses-banner { padding: 8px 10px; border-radius: var(--dt-radius-card); background: color-mix(in srgb, var(--rf-accent) 10%, transparent); }
9244
9329
  .dt-ses-banner p { margin: 0; overflow-wrap: anywhere; }
9245
- .dt-ses-strip:empty { display: none; }
9246
- .dt-ses-steps { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; color: var(--rf-text-muted); font-size: 12px; }
9247
- .dt-ses-steps li { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
9248
- .dt-ses-steps li > span { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
9330
+ .dt-ses-progress:empty { display: none; }
9331
+ .dt-ses-progress[data-fills-panel="true"] { align-self: center; width: min(100%, 280px); margin: auto; padding: 24px 0; }
9332
+ .dt-sheet-body:has(.dt-ses-progress[data-fills-panel="true"]) > .dt-sheet-foot { margin-top: 0; }
9333
+ .dt-ses-steps { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; color: var(--rf-text-muted); font-size: 12.5px; }
9334
+ .dt-ses-steps li { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
9335
+ .dt-ses-steps li > span:first-child { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
9249
9336
  .dt-ses-steps li::before { content: ""; flex: none; box-sizing: border-box; width: 10px; height: 10px; border: 1.5px solid currentColor; border-radius: 50%; }
9250
9337
  .dt-ses-steps li[data-state="done"]::before { border-color: var(--rf-accent); background: var(--rf-accent); }
9251
9338
  .dt-ses-steps li[data-state="active"] { color: var(--rf-text); }
9252
9339
  .dt-ses-steps li[data-state="active"]::before { border-color: var(--rf-accent); border-right-color: transparent; animation: dt-step-spin 800ms linear infinite; }
9253
9340
  .dt-ses-steps li[data-state="failed"] { color: var(--rf-danger); }
9254
9341
  .dt-ses-steps li[data-state="failed"]::before { background: currentColor; }
9342
+ .dt-ses-bar { position: relative; flex: 1 1 100%; height: 4px; margin-left: 18px; overflow: hidden; border-radius: var(--rf-pill); background: var(--rf-bg-hover); }
9343
+ .dt-ses-bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: var(--rf-accent); transition: width 300ms var(--rf-ease); }
9344
+ .dt-ses-bar > span[data-indeterminate="true"] { width: 35%; animation: dt-bar-slide 1.3s ease-in-out infinite; }
9255
9345
  @keyframes dt-step-spin { to { rotate: 360deg; } }
9256
- @media (prefers-reduced-motion: reduce) { .dt-ses-steps li::before { animation: none; } }
9346
+ @keyframes dt-bar-slide { from { translate: -100% 0; } to { translate: 300% 0; } }
9347
+ @media (prefers-reduced-motion: reduce) {
9348
+ .dt-ses-steps li::before { animation: none; }
9349
+ .dt-ses-bar > span[data-indeterminate="true"] { width: 100%; opacity: .4; animation: none; }
9350
+ }
9351
+ .dt-ses-openable { position: relative; transition: background-color 120ms var(--rf-ease); }
9352
+ .dt-ses-openable:has(> .dt-ses-open):hover { background: var(--rf-bg-hover); }
9353
+ .dt-ses-open { padding: 0; text-align: left; }
9354
+ .dt-ses-open::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
9355
+ .dt-ses-open:focus-visible { outline: none; }
9356
+ .dt-ses-open:focus-visible::after { outline: 2px solid var(--rf-accent); outline-offset: -2px; }
9357
+ .dt-ses-openable :is(a, button, input, select, textarea):not(.dt-ses-open, .dt-ses-hover-action) { position: relative; z-index: 1; }
9358
+ .dt-ses-openable > .dt-ses-hover-action {
9359
+ position: absolute; top: 6px; right: 6px; z-index: 2; width: 28px; height: 28px; opacity: 0;
9360
+ transition: opacity 120ms var(--rf-ease), background 160ms var(--rf-ease), color 160ms var(--rf-ease);
9361
+ }
9362
+ .dt-ses-hover-action svg { width: 14px; height: 14px; }
9363
+ .dt-ses-openable:hover > .dt-ses-hover-action, .dt-ses-openable > .dt-ses-hover-action:focus-visible { opacity: 1; }
9364
+ @media (hover: none) {
9365
+ .dt-ses-openable > .dt-ses-hover-action { opacity: 1; }
9366
+ .dt-ses-seg:has(> .dt-ses-hover-action) { padding-right: 40px; }
9367
+ }
9257
9368
  .dt-ses-rows { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
9258
9369
  .dt-ses-row {
9259
9370
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%;
@@ -9263,8 +9374,17 @@ button.dt-ses-row:hover { background: var(--rf-bg-hover); }
9263
9374
  .dt-ses-row .dt-card-title { max-width: 100%; }
9264
9375
  ${`
9265
9376
  .dt-ses-title { flex: 1; min-width: 0; margin: 0; font-size: 15px; font-weight: 650; overflow-wrap: anywhere; }
9377
+ .dt-ses-head > .dt-card-head { gap: 10px; }
9378
+ .dt-ses-head .dt-ses-title { flex: 0 1 auto; overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
9266
9379
  .dt-ses-head .dt-menu > .icon-btn { width: 30px; height: 30px; }
9380
+ .dt-ses-facts { display: flex; flex: 1 0 auto; align-items: center; gap: 10px; margin: 0; color: var(--rf-text-muted); font-size: 12px; white-space: nowrap; }
9381
+ .dt-ses-facts > span { display: inline-flex; align-items: center; gap: 4px; }
9382
+ .dt-ses-facts svg { width: 13px; height: 13px; }
9383
+ .dt-ses-foot { flex-wrap: nowrap; }
9384
+ .dt-ses-foot > .dt-btn { flex: 1 1 0; justify-content: center; height: 32px; }
9267
9385
  .dt-btn[data-icon-only] { justify-content: center; width: 28px; padding: 0; }
9386
+ .dt-btn[aria-pressed="true"] { background: color-mix(in srgb, var(--rf-text) 16%, transparent); }
9387
+ .dt-btn[data-variant="primary"][aria-pressed="true"] { background: var(--rf-success); color: #fff; }
9268
9388
  .dt-ses-field { display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; color: var(--rf-text-muted); }
9269
9389
  .dt-ses-need {
9270
9390
  margin: 0; font-weight: 600; overflow-wrap: anywhere;
@@ -9279,7 +9399,7 @@ ${`
9279
9399
  .dt-ses-details section { display: flex; flex-direction: column; gap: 2px; }
9280
9400
  .dt-ses-quote { padding: 2px 0; font-size: 12px; text-align: left; overflow-wrap: anywhere; }
9281
9401
  .dt-ses-quote:hover { color: var(--rf-accent); }
9282
- .dt-ses-time { flex: none; color: var(--rf-text-muted); font: 11.5px ${MONO_FONT$1}; font-variant-numeric: tabular-nums; }
9402
+ .dt-ses-time { flex: none; align-self: flex-start; color: var(--rf-text-muted); font: 11.5px ${MONO_FONT$1}; font-variant-numeric: tabular-nums; }
9283
9403
  .dt-ses-verify { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 8px 0 0; border: 0; border-top: 1px solid var(--rf-border); }
9284
9404
  .dt-ses-timeline { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; }
9285
9405
  .dt-ses-seg { display: flex; gap: 8px; padding: 6px 8px; border-radius: var(--dt-radius-field); }
@@ -9289,10 +9409,18 @@ ${`
9289
9409
  `}${`
9290
9410
  .dt-ses-thumbs { display: flex; flex-wrap: wrap; gap: 6px; }
9291
9411
  .dt-ses-shot { position: relative; display: inline-block; max-width: 100%; line-height: 0; }
9292
- .dt-ses-shot img { display: block; max-width: 100%; height: auto; max-height: 132px; border-radius: var(--dt-radius-field); box-shadow: var(--rf-shadow-inset); }
9412
+ .dt-ses-shot img { display: block; width: auto; max-width: 100%; height: auto; max-height: 132px; border-radius: var(--dt-radius-field); box-shadow: var(--rf-shadow-inset); }
9293
9413
  .dt-ses-shot[data-large="true"] img { max-height: none; }
9294
9414
  .dt-ses-shot-mark { position: absolute; border: 2px solid var(--rf-accent); border-radius: var(--dt-radius-tiny); pointer-events: none; }
9295
- .dt-ses-shot-btn { align-self: flex-start; padding: 0; border-radius: var(--dt-radius-field); }
9415
+ .dt-ses-shot-face { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; width: 100%; }
9416
+ .dt-ses-caption { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; min-width: 0; color: var(--rf-text-muted); font-size: 11.5px; line-height: 1.4; overflow-wrap: anywhere; }
9417
+ .dt-ses-caption-time { display: inline-flex; align-items: center; gap: 4px; color: var(--rf-text); font-weight: 550; font-variant-numeric: tabular-nums; }
9418
+ .dt-ses-caption-time svg { width: 12px; height: 12px; color: var(--rf-text-muted); }
9419
+ .dt-ses-source-btn {
9420
+ display: flex; flex-direction: column; gap: 2px; margin: 0 -8px; padding: 6px 8px; border-radius: var(--dt-radius-field);
9421
+ text-align: left; transition: background-color 120ms var(--rf-ease);
9422
+ }
9423
+ .dt-ses-source-btn:hover { background: var(--rf-bg-hover); }
9296
9424
  .dt-ses-start { display: flex; flex-direction: column; gap: 8px; }
9297
9425
  .dt-ses-brief-entry > summary { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: var(--dt-radius-card); background: var(--rf-bg-subtle); cursor: pointer; list-style: none; }
9298
9426
  .dt-ses-brief-entry > summary::-webkit-details-marker { display: none; }
@@ -12271,6 +12399,7 @@ const DEVTOOLS_STYLES = `
12271
12399
  position: sticky; bottom: -10px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
12272
12400
  margin: auto -10px -10px; padding: 10px; border-top: 1px solid var(--rf-border); background: var(--rf-bg);
12273
12401
  }
12402
+ .dt-sheet-body:has(.dt-empty) > .dt-sheet-foot { margin-top: 0; }
12274
12403
  .dt-archived { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 10px; border: 1px dashed var(--rf-border); border-radius: var(--dt-radius-card); }
12275
12404
  .dt-archived .dt-meta { flex: 1 1 100%; }
12276
12405
  .dt-group { margin: 6px 2px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--rf-text-muted); }
@@ -13414,4 +13543,4 @@ function useSheetTab() {
13414
13543
  //#endregion
13415
13544
  export { sessionSheetPlace as A, teamTransport as B, AccountFooter as C, MissingRouteHint as D, MISSING_SECRET_KEY_HINT as E, useWorkspaceLoader as F, SessionApiError as G, disconnectBoard as H, purgeAccount as I, reportFailure as J, sessionApiErrorSchema as K, bridgeBoardCall as L, RecordingLayer as M, SessionWorkspaceProvider as N, SESSIONS_STYLES as O, failureMessage as P, bridgeTransport as R, InboxPanel as S, CONNECT_REQUIREMENT as T, probeDevRoute as U, callReflet as V, startBoardConnect as W, DevtoolsBarButtons as _, NotePicker as a, CodeTab as b, isActiveNote as c, useNotes as d, findOnPage as f, DevtoolsBar as g, DevtoolsSheet as h, NotesPanel as i, useSessionNavigation as j, SessionsPanel as k, isNoteOnPage as l, DEVTOOLS_STYLES as m, useSheetTab as n, enrichNote as o, usePageHighlight as p, Notices as q, NotePins as r, hasTriedToLocate as s, FIRST_SHEET_TAB as t, unsentNoteCount as u, SHEET_TABS as v, AccountNote as w, useCodeLookup as x, SessionsBarButton as y, devtoolsTransport as z };
13416
13545
 
13417
- //# sourceMappingURL=use-sheet-tab-CacYFVjI.js.map
13546
+ //# sourceMappingURL=use-sheet-tab-azUezLAq.js.map