summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui/CacheTimerBadge.svelte
blob: 9e6f96ddc343409e35b2d7577a97b8b6187e5678 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<script lang="ts">
  /**
   * One cache-timer badge. LIVE when given `since` (counts up from that epoch
   * ms, ticking once a second, and stops ticking at the 10-minute cap); STAMPED
   * when given `elapsedMs` (a finished gap — static). Formatting lives in
   * `core/metrics` (`viewCacheTimer`). Owning the tick here means only this
   * badge re-renders each second, not the transcript.
   */
  import { untrack } from "svelte";
  import { CACHE_TIMER_MAX_MS, viewCacheTimer } from "../../../core/metrics";

  let {
    since,
    elapsedMs,
    size = "sm",
    now = Date.now,
  }: {
    /** Live: epoch ms the gap started. */
    since?: number;
    /** Stamped: the gap's final duration (wins over `since`). */
    elapsedMs?: number;
    /** Badge size — match the neighbouring cache badge (`xs` on step lines). */
    size?: "xs" | "sm";
    /** Wall clock (injected for tests). */
    now?: () => number;
  } = $props();

  // Initial read only (the effect below keeps it current while live).
  let current = $state(untrack(() => now()));

  // Depends ONLY on `since`/`elapsedMs`/`now`. It must never READ `current`
  // (it writes it): reading its own write makes the effect re-run itself on
  // every clock change → `effect_update_depth_exceeded`, which aborts the whole
  // reactive flush and stalls the transcript (turn data rendering late).
  $effect(() => {
    if (elapsedMs !== undefined || since === undefined) return;
    const start = since;
    const clock = now;
    const t0 = clock();
    current = t0;
    if (t0 - start >= CACHE_TIMER_MAX_MS) return;
    const id = setInterval(() => {
      const t = clock();
      current = t;
      if (t - start >= CACHE_TIMER_MAX_MS) clearInterval(id);
    }, 1000);
    return () => clearInterval(id);
  });

  const badgeClass = {
    success: "badge-success",
    warning: "badge-warning",
    error: "badge-error",
  } as const;

  const live = $derived(elapsedMs === undefined);
  const view = $derived(viewCacheTimer(elapsedMs ?? current - (since ?? current)));
</script>

<span
  class="badge font-mono tabular-nums {badgeClass[view.level]}"
  class:badge-xs={size === "xs"}
  class:badge-sm={size === "sm"}
  class:badge-outline={!live}
  title={live
    ? "Cache timer (running): time since the last model request. The Claude prompt cache expires after 5 minutes idle."
    : "Cache timer (stopped): how long the cache sat idle before the next request. The Claude prompt cache expires after 5 minutes idle."}
  data-testid={live ? "cache-timer-live" : "cache-timer-stamp"}
  >{view.label}</span
>