summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui/CacheTimerBadge.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-24 16:55:38 +0900
committerAdam Malczewski <[email protected]>2026-10-24 16:55:38 +0900
commit83442d00d9d9c7f0ca200d6a2e8b42195708493f (patch)
treeebeeef6c49d4cc3f5974e59b1156de3e5e4c7c6d /src/features/chat/ui/CacheTimerBadge.svelte
parenta4aa86e66dad93f40bce640545d8903f7e6bcb64 (diff)
parentc8b2fb49835bde2f85d99a91b313e6a01ce1359c (diff)
downloaddispatch-web-83442d00d9d9c7f0ca200d6a2e8b42195708493f.tar.gz
dispatch-web-83442d00d9d9c7f0ca200d6a2e8b42195708493f.zip
Merge local dev into feature/close-tab-cascade-subagents
Diffstat (limited to 'src/features/chat/ui/CacheTimerBadge.svelte')
-rw-r--r--src/features/chat/ui/CacheTimerBadge.svelte70
1 files changed, 70 insertions, 0 deletions
diff --git a/src/features/chat/ui/CacheTimerBadge.svelte b/src/features/chat/ui/CacheTimerBadge.svelte
new file mode 100644
index 0000000..9e6f96d
--- /dev/null
+++ b/src/features/chat/ui/CacheTimerBadge.svelte
@@ -0,0 +1,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
+>