favicon.ts

 1// Favicon service for dynamic status indication
 2// Modifies the server-injected favicon to show a colored dot when the agent state changes
 3
 4type FaviconStatus = "working" | "ready";
 5
 6let currentStatus: FaviconStatus = "ready";
 7let originalSVG: string | null = null;
 8
 9// Get the existing favicon link (injected by server)
10function getFaviconLink(): HTMLLinkElement | null {
11  return document.querySelector('link[rel="icon"]');
12}
13
14// Extract and decode the SVG from the data URI
15function extractSVGFromDataURI(dataURI: string): string | null {
16  if (!dataURI.startsWith("data:image/svg+xml,")) {
17    return null;
18  }
19  try {
20    return decodeURIComponent(dataURI.substring("data:image/svg+xml,".length));
21  } catch {
22    return null;
23  }
24}
25
26// Add a status dot to the SVG
27function addStatusDot(svg: string, status: FaviconStatus): string {
28  // Remove the closing </svg> tag
29  const closingTagIndex = svg.lastIndexOf("</svg>");
30  if (closingTagIndex === -1) {
31    return svg;
32  }
33
34  const svgWithoutClose = svg.substring(0, closingTagIndex);
35
36  // Add the status dot in the bottom-right corner
37  // New viewBox is 400x400, so position dot near bottom-right at ~350,350
38  const dotColor = status === "working" ? "#f59e0b" : "#22c55e";
39  const statusDot = `
40  <circle cx="340" cy="340" r="50" fill="white"/>
41  <circle cx="340" cy="340" r="40" fill="${dotColor}"/>
42`;
43
44  return svgWithoutClose + statusDot + "</svg>";
45}
46
47// Update the favicon to reflect the current status
48export function setFaviconStatus(status: FaviconStatus): void {
49  if (status === currentStatus && originalSVG !== null) {
50    return;
51  }
52
53  const link = getFaviconLink();
54  if (!link) {
55    return;
56  }
57
58  // Capture the original SVG on first call
59  if (originalSVG === null) {
60    const extracted = extractSVGFromDataURI(link.href);
61    if (extracted) {
62      originalSVG = extracted;
63    } else {
64      // If we can't extract SVG, give up
65      return;
66    }
67  }
68
69  currentStatus = status;
70
71  // Generate new SVG with status dot
72  const newSVG = addStatusDot(originalSVG, status);
73  const newDataURI = "data:image/svg+xml," + encodeURIComponent(newSVG);
74
75  // Update the favicon
76  link.href = newDataURI;
77}
78
79// Initialize the favicon service (call on app start)
80export function initializeFavicon(): void {
81  // Wait a tick for the server-injected favicon to be present
82  setTimeout(() => {
83    setFaviconStatus("ready");
84  }, 0);
85}