Skip to content

Instantly share code, notes, and snippets.

@simonw
Created December 19, 2025 04:30
Show Gist options
  • Select an option

  • Save simonw/0d25251f813aaded363c1a4319e844b0 to your computer and use it in GitHub Desktop.

Select an option

Save simonw/0d25251f813aaded363c1a4319e844b0 to your computer and use it in GitHub Desktop.
{"type":"file-history-snapshot","messageId":"662cbb96-5dbb-415a-9dd3-8b2275c5f1b0","snapshot":{"messageId":"662cbb96-5dbb-415a-9dd3-8b2275c5f1b0","trackedFileBackups":{},"timestamp":"2025-12-19T04:25:55.259Z"},"isSnapshotUpdate":false}
{"parentUuid":null,"isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","type":"user","message":{"role":"user","content":"In claude-code-timeline the feature that gathers all user prompts should skip any prompts that have \"isCompactSummary\": true in their JSON object"},"uuid":"662cbb96-5dbb-415a-9dd3-8b2275c5f1b0","timestamp":"2025-12-19T04:25:55.254Z","thinkingMetadata":{"level":"high","disabled":false,"triggers":[]},"todos":[]}
{"parentUuid":"662cbb96-5dbb-415a-9dd3-8b2275c5f1b0","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","message":{"model":"claude-opus-4-5-20251101","id":"msg_012YPjy7RJVTbSCtPADcuqq6","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to modify the claude-code-timeline tool to skip prompts that have \"isCompactSummary\": true in their JSON object. Let me first find and read the relevant code to understand how it currently works.","signature":"Ev4CCkYIChgCKkBIP75V7ytf9BQppxDCgSJr6ab0RwfRkDldxTNnvanm2qYYbNu6mkH+4PlUNIQ6qHCXWYZWXhwuBpCOcbjDbSFiEgxLYVW0RF56bBFSV0caDF/VLDLn0IsZeLbNUyIwbYgJ5NROBLgJeNWmqfHGVKMs5lZ0iV67qhpzh+lHrEYbJp8KjxZ3vixNbdwlB3kiKuUBws6Hyn1e/M+VqiXyMA878NPoZDgLYtuU01uY4Jycw6QxsxudgYOo5Ve62A5fHwns44yUoZKh9kJIPDBKgPyzR7cBOcxxoKS2j1+5oGjvkjHtw+FAVRmRM9XiElm4jRGKOxc4+xRCkz6nIMmgomVc0HwRoHy6mDfYFAKFil83f/z3BulltvcCX5PPOGG+EYmT2tmeFdGkE1eGu+y/ErO/mAl3d1ALbxI5ZIklR1emN9VKims9eFy/An9tbA1AovcppOA8nJC51hCwzMD5mK75kYoylhMjTm3rtcWvU8DXCd2HPIHXERgB"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":3752,"cache_read_input_tokens":14737,"cache_creation":{"ephemeral_5m_input_tokens":3752,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}},"requestId":"req_011CWFQpPodW6EK88pUzARio","type":"assistant","uuid":"f1cfeafe-a9d4-4258-9afe-c59d55e84116","timestamp":"2025-12-19T04:25:59.433Z"}
{"parentUuid":"f1cfeafe-a9d4-4258-9afe-c59d55e84116","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","message":{"model":"claude-opus-4-5-20251101","id":"msg_012YPjy7RJVTbSCtPADcuqq6","type":"message","role":"assistant","content":[{"type":"text","text":"Let me find the claude-code-timeline code and understand how it currently gathers user prompts."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":3752,"cache_read_input_tokens":14737,"cache_creation":{"ephemeral_5m_input_tokens":3752,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}},"requestId":"req_011CWFQpPodW6EK88pUzARio","type":"assistant","uuid":"d05a92d4-74cb-489d-b236-ed95032c454a","timestamp":"2025-12-19T04:26:00.174Z"}
{"parentUuid":"d05a92d4-74cb-489d-b236-ed95032c454a","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","message":{"model":"claude-opus-4-5-20251101","id":"msg_012YPjy7RJVTbSCtPADcuqq6","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UP56J6aeXvc5df8PnNgNDf","name":"Glob","input":{"pattern":"**/claude-code-timeline/**/*.py"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":3752,"cache_read_input_tokens":14737,"cache_creation":{"ephemeral_5m_input_tokens":3752,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}},"requestId":"req_011CWFQpPodW6EK88pUzARio","type":"assistant","uuid":"12f2fbb9-3805-4061-973d-ccc4cf0b09f5","timestamp":"2025-12-19T04:26:00.827Z"}
{"parentUuid":"12f2fbb9-3805-4061-973d-ccc4cf0b09f5","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_012YPjy7RJVTbSCtPADcuqq6","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WQUTYGWmxy6eC1v2mvrqof","name":"Glob","input":{"pattern":"**/claude-code-timeline*.py"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":3752,"cache_read_input_tokens":14737,"cache_creation":{"ephemeral_5m_input_tokens":3752,"ephemeral_1h_input_tokens":0},"output_tokens":183,"service_tier":"standard"}},"requestId":"req_011CWFQpPodW6EK88pUzARio","type":"assistant","uuid":"a78737a2-9d6f-4dda-a04b-915d66cce4f1","timestamp":"2025-12-19T04:26:01.308Z"}
{"parentUuid":"a78737a2-9d6f-4dda-a04b-915d66cce4f1","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WQUTYGWmxy6eC1v2mvrqof","type":"tool_result","content":"No files found"}]},"uuid":"b52f4cc3-0d68-4dbc-8ca0-576dec804177","timestamp":"2025-12-19T04:26:01.368Z","toolUseResult":{"filenames":[],"durationMs":38,"numFiles":0,"truncated":false}}
{"parentUuid":"b52f4cc3-0d68-4dbc-8ca0-576dec804177","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UP56J6aeXvc5df8PnNgNDf","type":"tool_result","content":"No files found"}]},"uuid":"10eee9e5-6170-4ab0-97b7-8b83708c8515","timestamp":"2025-12-19T04:26:01.372Z","toolUseResult":{"filenames":[],"durationMs":43,"numFiles":0,"truncated":false}}
{"parentUuid":"10eee9e5-6170-4ab0-97b7-8b83708c8515","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01G33Yaz2p3VWkRe6rL9q3wo","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Let me search more broadly for claude-code-timeline.","signature":"EtwBCkYIChgCKkBmS3BpsYoel245r+suL1e5LRjzr+s4oSSOUxIKiYGe9A1xSmrSAsXjf3g6DEfF5OveIZY1N3ICID8V/hNXQjA8Egw7ZCjPsyQ8zWux9VkaDKbokX2ch2AgpDNiNSIwY1Hxp4tnpMOjA15/1C+rfWXB63uuqxck58cyyjAKFTNAqBAPydkLr6GEBJAlfvseKkSioNJqvPWzI6mNILalm0iAtLAKW5I6Ddm0RSXTBWFJOGcDbUKisT3LkCAT908akeLq/P5UfZ6CC2yWRa1xbWZ+mnFClBgB"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":41,"cache_creation_input_tokens":226,"cache_read_input_tokens":18489,"cache_creation":{"ephemeral_5m_input_tokens":226,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}},"requestId":"req_011CWFQpr3hcxeroxmmsgJDa","type":"assistant","uuid":"a197e623-311d-41fe-91cc-55ef12df0261","timestamp":"2025-12-19T04:26:04.090Z"}
{"parentUuid":"a197e623-311d-41fe-91cc-55ef12df0261","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01G33Yaz2p3VWkRe6rL9q3wo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Exw9yfZVoPxzgo28FPLYnp","name":"Glob","input":{"pattern":"**/*claude*timeline*"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":41,"cache_creation_input_tokens":226,"cache_read_input_tokens":18489,"cache_creation":{"ephemeral_5m_input_tokens":226,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}},"requestId":"req_011CWFQpr3hcxeroxmmsgJDa","type":"assistant","uuid":"f1e4fbfc-68b7-40f9-9a49-6814578ce387","timestamp":"2025-12-19T04:26:04.692Z"}
{"parentUuid":"f1e4fbfc-68b7-40f9-9a49-6814578ce387","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01G33Yaz2p3VWkRe6rL9q3wo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01YbjZUx1YNUGjJZfaofWris","name":"Grep","input":{"pattern":"claude-code-timeline"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":41,"cache_creation_input_tokens":226,"cache_read_input_tokens":18489,"cache_creation":{"ephemeral_5m_input_tokens":226,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}},"requestId":"req_011CWFQpr3hcxeroxmmsgJDa","type":"assistant","uuid":"bd1dab7a-63ac-4a3f-b215-7f25c5a05af8","timestamp":"2025-12-19T04:26:05.394Z"}
{"parentUuid":"bd1dab7a-63ac-4a3f-b215-7f25c5a05af8","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01YbjZUx1YNUGjJZfaofWris","type":"tool_result","content":"Found 2 files\npython/claude_code_to_gist.py\npython/README.md"}]},"uuid":"85eccbca-9c14-4234-8ba3-c072f0375402","timestamp":"2025-12-19T04:26:05.463Z","toolUseResult":{"mode":"files_with_matches","filenames":["python/claude_code_to_gist.py","python/README.md"],"numFiles":2}}
{"parentUuid":"85eccbca-9c14-4234-8ba3-c072f0375402","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Exw9yfZVoPxzgo28FPLYnp","type":"tool_result","content":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html\n/Users/simon/Dropbox/dev/tools/claude-code-timeline.docs.md"}]},"uuid":"11f2d729-65dc-4d75-b8e8-af211fbe1693","timestamp":"2025-12-19T04:26:05.471Z","toolUseResult":{"filenames":["/Users/simon/Dropbox/dev/tools/claude-code-timeline.html","/Users/simon/Dropbox/dev/tools/claude-code-timeline.docs.md"],"durationMs":35,"numFiles":2,"truncated":false}}
{"parentUuid":"11f2d729-65dc-4d75-b8e8-af211fbe1693","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_017g158AQVgmbGmdhs6g7SuG","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Good, found some files. The main file seems to be `claude-code-timeline.html`. Let me also check `claude_code_to_gist.py` since it might be related. Let me read the HTML file to find the user prompt gathering logic.","signature":"EoADCkYIChgCKkBKg/T/Be6iLNWuQXtaeEDc0DIM6jdfXR4z7SZR0iGlW396829kw42pFpnOoWbWRfU8w28jSaey70NdiRPJvFiQEgyVM2Dwt2x17RW52Y0aDEmOniJhRjtCiIg4TyIwDIJHhvcTC36n3I9Iln32msPwTdOpo/dYTlnYZx+a/amiknNbizRj7aiUAHH7EL7eKucBzhoEXTzXBm0Uie5ijTdkZPsQyZnFE+J3hB8DawdoxjNu1Yr+dv7meoX+o8zR+0Vq4ug6QDPC7MvmFXUQB/brYynCF7yoMTgLXwgnnvvklI+vl9KsjEaOA2lTo+c76XS0jovmVuk+pIBl1auVneyf7JkdGbIK4ACFMimVOkmo56fPfEnmgbvalp5q83A7hYgjdsXyLxvmDGkzZj5NOFsj8J87biMGbjcnbETqx/+ZsaWNxmx3JrZ1WLG8O+4r9+NKABC50ZrQT3+aSbNNskQifbCQ2PeiDXRxkvFYQ0pzxq/kjufvIpJwGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":62,"cache_creation_input_tokens":229,"cache_read_input_tokens":18715,"cache_creation":{"ephemeral_5m_input_tokens":229,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard"}},"requestId":"req_011CWFQq9HHeifjdmMSgLj6h","type":"assistant","uuid":"01c56e1b-8be0-44e5-8b62-e97a45490805","timestamp":"2025-12-19T04:26:09.359Z"}
{"parentUuid":"01c56e1b-8be0-44e5-8b62-e97a45490805","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_017g158AQVgmbGmdhs6g7SuG","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014KMR6tkEngWjdZHbqHS7vN","name":"Read","input":{"file_path":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":62,"cache_creation_input_tokens":229,"cache_read_input_tokens":18715,"cache_creation":{"ephemeral_5m_input_tokens":229,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard"}},"requestId":"req_011CWFQq9HHeifjdmMSgLj6h","type":"assistant","uuid":"8bf37d6d-8dbe-4e89-89fa-843e79986451","timestamp":"2025-12-19T04:26:09.771Z"}
{"parentUuid":"8bf37d6d-8dbe-4e89-89fa-843e79986451","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014KMR6tkEngWjdZHbqHS7vN","type":"tool_result","content":" 1→<!DOCTYPE html>\n 2→<html lang=\"en\">\n 3→<head>\n 4→ <meta charset=\"UTF-8\" />\n 5→ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n 6→ <title>Claude Code Timeline Viewer</title>\n 7→ <script src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.3/purify.min.js\" integrity=\"sha384-osZDKVu4ipZP703HmPOhWdyBajcFyjX2Psjk//TG1Rc0AdwEtuToaylrmcK3LdAl\" crossorigin=\"anonymous\"></script>\n 8→ <script src=\"https://cdnjs.cloudflare.com/ajax/libs/markdown-it/4.4.0/markdown-it.min.js\" integrity=\"sha384-cxGp+QDeMxACP73aA0Lj0Sc/h4z1X73+umcvCyI0Vbyh3OYxgrPI+0mxG9slsMTN\" crossorigin=\"anonymous\"></script>\n 9→ <style>\n 10→ :root {\n 11→ color-scheme: light dark;\n 12→ --bg: #0b0c0f;\n 13→ --panel: #12141a;\n 14→ --panel-2: #0f1116;\n 15→ --text: #e8eaf0;\n 16→ --muted: #a6adbf;\n 17→ --line: rgba(255, 255, 255, 0.10);\n 18→ --line-2: rgba(255, 255, 255, 0.16);\n 19→ --accent: #6aa9ff;\n 20→ --accent-2: #8a5cff;\n 21→ --good: #46d39a;\n 22→ --warn: #ffcc66;\n 23→ --bad: #ff6b6b;\n 24→ --shadow: 0 18px 50px rgba(0, 0, 0, 0.35);\n 25→ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n 26→ \"Courier New\", monospace;\n 27→ --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,\n 28→ \"Apple Color Emoji\", \"Segoe UI Emoji\";\n 29→ }\n 30→\n 31→ @media (prefers-color-scheme: light) {\n 32→ :root {\n 33→ --bg: #f7f8fc;\n 34→ --panel: #ffffff;\n 35→ --panel-2: #ffffff;\n 36→ --text: #10121a;\n 37→ --muted: #5b647a;\n 38→ --line: rgba(16, 18, 26, 0.12);\n 39→ --line-2: rgba(16, 18, 26, 0.18);\n 40→ --shadow: 0 18px 50px rgba(16, 18, 26, 0.10);\n 41→ }\n 42→ }\n 43→\n 44→ * { box-sizing: border-box; }\n 45→ html, body { height: 100%; }\n 46→ body {\n 47→ margin: 0;\n 48→ font-family: var(--sans);\n 49→ background: radial-gradient(1200px 600px at 10% -10%, rgba(106, 169, 255, 0.18), transparent 45%),\n 50→ radial-gradient(1200px 600px at 90% -20%, rgba(138, 92, 255, 0.16), transparent 50%),\n 51→ var(--bg);\n 52→ color: var(--text);\n 53→ }\n 54→\n 55→ .wrap {\n 56→ max-width: none;\n 57→ margin: 0;\n 58→ padding: 22px 16px 42px;\n 59→ }\n 60→\n 61→ header {\n 62→ display: grid;\n 63→ gap: 10px;\n 64→ margin-bottom: 14px;\n 65→ }\n 66→\n 67→ h1 {\n 68→ margin: 0;\n 69→ font-size: clamp(22px, 3vw, 32px);\n 70→ letter-spacing: 0.2px;\n 71→ }\n 72→\n 73→ .sub {\n 74→ margin: 6px 0 0;\n 75→ color: var(--muted);\n 76→ line-height: 1.35;\n 77→ max-width: 70ch;\n 78→ }\n 79→\n 80→ .controls {\n 81→ display: flex;\n 82→ flex-wrap: wrap;\n 83→ gap: 10px;\n 84→ align-items: center;\n 85→ justify-content: flex-start;\n 86→ }\n 87→\n 88→ .controls .field {\n 89→ display: grid;\n 90→ gap: 4px;\n 91→ min-width: 180px;\n 92→ }\n 93→\n 94→ .label {\n 95→ font-size: 12px;\n 96→ color: var(--muted);\n 97→ }\n 98→\n 99→ input[type=\"search\"], input[type=\"url\"], select, textarea {\n 100→ font: inherit;\n 101→ color: inherit;\n 102→ border: 1px solid var(--line);\n 103→ background: color-mix(in srgb, var(--panel) 92%, transparent);\n 104→ border-radius: 10px;\n 105→ padding: 10px 11px;\n 106→ outline: none;\n 107→ }\n 108→\n 109→ input[type=\"search\"]:focus, input[type=\"url\"]:focus, select:focus, textarea:focus {\n 110→ border-color: color-mix(in srgb, var(--accent) 60%, var(--line));\n 111→ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n 112→ }\n 113→\n 114→ .segmented {\n 115→ display: inline-flex;\n 116→ border: 1px solid var(--line);\n 117→ border-radius: 999px;\n 118→ overflow: hidden;\n 119→ background: color-mix(in srgb, var(--panel) 92%, transparent);\n 120→ }\n 121→\n 122→ .segmented button {\n 123→ border: none;\n 124→ padding: 9px 12px;\n 125→ font: inherit;\n 126→ background: transparent;\n 127→ color: var(--muted);\n 128→ cursor: pointer;\n 129→ min-width: 74px;\n 130→ }\n 131→\n 132→ .segmented button.active {\n 133→ background: color-mix(in srgb, var(--accent) 16%, transparent);\n 134→ color: var(--text);\n 135→ }\n 136→\n 137→ .segmented button:focus-visible {\n 138→ outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);\n 139→ outline-offset: -2px;\n 140→ }\n 141→\n 142→ .btn {\n 143→ border: 1px solid var(--line);\n 144→ background: color-mix(in srgb, var(--panel) 92%, transparent);\n 145→ color: var(--text);\n 146→ border-radius: 10px;\n 147→ padding: 10px 12px;\n 148→ font: inherit;\n 149→ cursor: pointer;\n 150→ transition: transform 80ms ease, border-color 120ms ease;\n 151→ }\n 152→\n 153→ .btn:hover { border-color: var(--line-2); }\n 154→ .btn:active { transform: translateY(1px); }\n 155→ .btn.primary {\n 156→ border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n 157→ background: color-mix(in srgb, var(--accent) 14%, transparent);\n 158→ }\n 159→\n 160→ .btn:disabled {\n 161→ opacity: 0.55;\n 162→ cursor: not-allowed;\n 163→ }\n 164→\n 165→ .panel {\n 166→ background: var(--panel);\n 167→ border: 1px solid var(--line);\n 168→ border-radius: 16px;\n 169→ box-shadow: var(--shadow);\n 170→ }\n 171→\n 172→ .input-panel {\n 173→ padding: 14px;\n 174→ margin-bottom: 14px;\n 175→ }\n 176→\n 177→ .filters-panel {\n 178→ padding: 14px;\n 179→ margin-bottom: 14px;\n 180→ }\n 181→\n 182→ .input-grid {\n 183→ display: grid;\n 184→ grid-template-columns: 1fr;\n 185→ gap: 14px;\n 186→ align-items: stretch;\n 187→ }\n 188→\n 189→ @media (min-width: 920px) {\n 190→ .input-grid {\n 191→ grid-template-columns: 380px 1fr;\n 192→ }\n 193→ }\n 194→\n 195→ .dropzone {\n 196→ border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--line));\n 197→ border-radius: 14px;\n 198→ background: linear-gradient(\n 199→ 135deg,\n 200→ color-mix(in srgb, var(--accent) 12%, transparent),\n 201→ transparent 45%,\n 202→ color-mix(in srgb, var(--accent-2) 10%, transparent)\n 203→ );\n 204→ padding: 16px;\n 205→ display: grid;\n 206→ gap: 6px;\n 207→ cursor: pointer;\n 208→ min-height: 124px;\n 209→ align-content: center;\n 210→ }\n 211→\n 212→ .dropzone.dragover {\n 213→ border-style: solid;\n 214→ border-color: color-mix(in srgb, var(--accent) 70%, var(--line));\n 215→ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n 216→ }\n 217→\n 218→ .dropzone strong { font-size: 15px; }\n 219→ .muted { color: var(--muted); }\n 220→ .tiny { font-size: 12px; color: var(--muted); }\n 221→\n 222→ .tabs {\n 223→ display: flex;\n 224→ gap: 8px;\n 225→ margin-bottom: 10px;\n 226→ flex-wrap: wrap;\n 227→ align-items: center;\n 228→ }\n 229→\n 230→ .tab {\n 231→ border: 1px solid var(--line);\n 232→ background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n 233→ color: var(--muted);\n 234→ border-radius: 999px;\n 235→ padding: 7px 10px;\n 236→ cursor: pointer;\n 237→ font: inherit;\n 238→ }\n 239→\n 240→ .tab.active {\n 241→ color: var(--text);\n 242→ border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n 243→ background: color-mix(in srgb, var(--accent) 12%, transparent);\n 244→ }\n 245→\n 246→ textarea {\n 247→ width: 100%;\n 248→ min-height: 190px;\n 249→ resize: vertical;\n 250→ font-family: var(--mono);\n 251→ font-size: 12px;\n 252→ line-height: 1.4;\n 253→ }\n 254→\n 255→ .row {\n 256→ display: flex;\n 257→ gap: 10px;\n 258→ flex-wrap: wrap;\n 259→ align-items: center;\n 260→ margin-top: 10px;\n 261→ }\n 262→\n 263→ .status {\n 264→ margin-top: 12px;\n 265→ padding: 10px 12px;\n 266→ border-radius: 12px;\n 267→ border: 1px solid var(--line);\n 268→ background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n 269→ display: none;\n 270→ white-space: pre-wrap;\n 271→ word-break: break-word;\n 272→ }\n 273→ .status.visible { display: block; }\n 274→ .status.error { border-color: color-mix(in srgb, var(--bad) 60%, var(--line)); }\n 275→ .status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n 276→ .status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n 277→\n 278→ main {\n 279→ display: grid;\n 280→ gap: 14px;\n 281→ grid-template-columns: 1fr;\n 282→ min-height: 560px;\n 283→ }\n 284→\n 285→ @media (min-width: 1060px) {\n 286→ main {\n 287→ grid-template-columns: 1fr 520px;\n 288→ align-items: stretch;\n 289→ }\n 290→ }\n 291→\n 292→ .timeline-panel {\n 293→ display: grid;\n 294→ grid-template-rows: auto 1fr;\n 295→ min-height: 560px;\n 296→ }\n 297→\n 298→ .stats {\n 299→ display: flex;\n 300→ gap: 12px;\n 301→ flex-wrap: wrap;\n 302→ align-items: baseline;\n 303→ justify-content: space-between;\n 304→ padding: 12px 14px;\n 305→ border-bottom: 1px solid var(--line);\n 306→ }\n 307→\n 308→ .stats .big {\n 309→ font-size: 14px;\n 310→ font-weight: 600;\n 311→ }\n 312→\n 313→ .timeline {\n 314→ overflow: auto;\n 315→ padding: 8px 0;\n 316→ }\n 317→\n 318→ .timeline:focus-visible {\n 319→ outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);\n 320→ outline-offset: -2px;\n 321→ border-radius: 14px;\n 322→ }\n 323→\n 324→ .day-sep {\n 325→ position: sticky;\n 326→ top: 0;\n 327→ z-index: 2;\n 328→ padding: 8px 14px;\n 329→ background: color-mix(in srgb, var(--panel) 92%, transparent);\n 330→ border-top: 1px solid var(--line);\n 331→ border-bottom: 1px solid var(--line);\n 332→ font-size: 12px;\n 333→ color: var(--muted);\n 334→ backdrop-filter: blur(6px);\n 335→ }\n 336→\n 337→ .event {\n 338→ display: grid;\n 339→ grid-template-columns: 176px 1fr;\n 340→ gap: 12px;\n 341→ padding: 10px 14px;\n 342→ border-bottom: 1px solid color-mix(in srgb, var(--line) 85%, transparent);\n 343→ cursor: pointer;\n 344→ }\n 345→\n 346→ .event:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }\n 347→ .event.selected {\n 348→ background: color-mix(in srgb, var(--accent) 12%, transparent);\n 349→ outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);\n 350→ outline-offset: -2px;\n 351→ }\n 352→\n 353→ .time {\n 354→ font-family: var(--mono);\n 355→ font-size: 12px;\n 356→ line-height: 1.35;\n 357→ color: var(--muted);\n 358→ display: grid;\n 359→ gap: 3px;\n 360→ }\n 361→\n 362→ .time .ts {\n 363→ color: var(--text);\n 364→ font-weight: 600;\n 365→ }\n 366→\n 367→ .badges {\n 368→ display: flex;\n 369→ gap: 6px;\n 370→ flex-wrap: wrap;\n 371→ align-items: center;\n 372→ margin-bottom: 6px;\n 373→ }\n 374→\n 375→ .badge {\n 376→ font-size: 11px;\n 377→ font-family: var(--mono);\n 378→ padding: 2px 7px;\n 379→ border-radius: 999px;\n 380→ border: 1px solid var(--line);\n 381→ color: var(--muted);\n 382→ background: color-mix(in srgb, var(--panel-2) 90%, transparent);\n 383→ }\n 384→\n 385→ .badge.k-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n 386→ .badge.k-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n 387→ .badge.k-file-history-snapshot { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); color: color-mix(in srgb, var(--warn) 75%, var(--text)); }\n 388→\n 389→ .badge.ct-text { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); color: color-mix(in srgb, var(--good) 75%, var(--text)); }\n 390→ .badge.ct-thinking { border-color: color-mix(in srgb, var(--accent-2) 55%, var(--line)); color: color-mix(in srgb, var(--accent-2) 75%, var(--text)); }\n 391→ .badge.ct-tool_use { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); color: color-mix(in srgb, var(--accent) 75%, var(--text)); }\n 392→ .badge.ct-tool_result { border-color: color-mix(in srgb, #8690a8 55%, var(--line)); color: color-mix(in srgb, #8690a8 70%, var(--text)); }\n 393→ .badge.ct-image { border-color: color-mix(in srgb, #ff9f43 55%, var(--line)); color: color-mix(in srgb, #ff9f43 75%, var(--text)); }\n 394→\n 395→ .badge.role-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n 396→ .badge.role-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n 397→\n 398→ .summary {\n 399→ font-size: 13px;\n 400→ line-height: 1.35;\n 401→ color: var(--text);\n 402→ word-break: break-word;\n 403→ }\n 404→\n 405→ .summary code {\n 406→ font-family: var(--mono);\n 407→ font-size: 12px;\n 408→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 409→ border: 1px solid var(--line);\n 410→ padding: 2px 5px;\n 411→ border-radius: 7px;\n 412→ }\n 413→\n 414→ .detail {\n 415→ display: grid;\n 416→ grid-template-rows: auto auto auto auto 1fr;\n 417→ min-height: 560px;\n 418→ }\n 419→\n 420→ @media (min-width: 1060px) {\n 421→ .detail {\n 422→ position: sticky;\n 423→ top: 16px;\n 424→ align-self: start;\n 425→ max-height: calc(100vh - 32px);\n 426→ }\n 427→ }\n 428→\n 429→ .detail.inline {\n 430→ position: static;\n 431→ top: auto;\n 432→ align-self: auto;\n 433→ max-height: none;\n 434→ min-height: 0;\n 435→ margin: 8px 14px 12px;\n 436→ box-shadow: none;\n 437→ }\n 438→\n 439→ .detail.inline pre {\n 440→ max-height: 52vh;\n 441→ }\n 442→\n 443→ .detail-head {\n 444→ padding: 12px 14px;\n 445→ border-bottom: 1px solid var(--line);\n 446→ display: grid;\n 447→ gap: 8px;\n 448→ }\n 449→\n 450→ .detail-title {\n 451→ font-size: 14px;\n 452→ font-weight: 700;\n 453→ line-height: 1.25;\n 454→ margin: 0;\n 455→ overflow: hidden;\n 456→ text-overflow: ellipsis;\n 457→ white-space: nowrap;\n 458→ }\n 459→\n 460→ .detail-actions {\n 461→ display: flex;\n 462→ gap: 8px;\n 463→ flex-wrap: wrap;\n 464→ align-items: center;\n 465→ justify-content: flex-start;\n 466→ }\n 467→\n 468→ .check {\n 469→ display: inline-flex;\n 470→ align-items: center;\n 471→ gap: 6px;\n 472→ color: var(--muted);\n 473→ font-size: 12px;\n 474→ user-select: none;\n 475→ }\n 476→\n 477→ pre {\n 478→ margin: 0;\n 479→ padding: 12px 14px 16px;\n 480→ overflow: auto;\n 481→ font-family: var(--mono);\n 482→ font-size: 12px;\n 483→ line-height: 1.45;\n 484→ color: color-mix(in srgb, var(--text) 92%, transparent);\n 485→ background: transparent;\n 486→ white-space: pre-wrap;\n 487→ word-break: break-word;\n 488→ }\n 489→\n 490→ .detail-images {\n 491→ display: flex;\n 492→ gap: 10px;\n 493→ padding: 12px 14px;\n 494→ border-bottom: 1px solid var(--line);\n 495→ overflow-x: auto;\n 496→ overscroll-behavior-x: contain;\n 497→ }\n 498→\n 499→ .thumb {\n 500→ border: 1px solid var(--line);\n 501→ background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n 502→ border-radius: 12px;\n 503→ padding: 0;\n 504→ cursor: pointer;\n 505→ width: 78px;\n 506→ height: 58px;\n 507→ overflow: hidden;\n 508→ flex: 0 0 auto;\n 509→ display: grid;\n 510→ place-items: stretch;\n 511→ }\n 512→\n 513→ .thumb:hover { border-color: var(--line-2); }\n 514→\n 515→ .thumb img {\n 516→ width: 100%;\n 517→ height: 100%;\n 518→ display: block;\n 519→ object-fit: cover;\n 520→ }\n 521→\n 522→ .image-modal {\n 523→ position: fixed;\n 524→ inset: 0;\n 525→ z-index: 9999;\n 526→ background: rgba(0, 0, 0, 0.68);\n 527→ display: none;\n 528→ padding: 18px;\n 529→ place-items: center;\n 530→ }\n 531→\n 532→ .image-modal.open { display: grid; }\n 533→\n 534→ .image-modal .dialog {\n 535→ position: relative;\n 536→ border-radius: 16px;\n 537→ border: 1px solid var(--line);\n 538→ background: color-mix(in srgb, var(--panel) 98%, transparent);\n 539→ box-shadow: var(--shadow);\n 540→ padding: 14px;\n 541→ max-width: min(1200px, 96vw);\n 542→ max-height: 92vh;\n 543→ display: grid;\n 544→ place-items: center;\n 545→ }\n 546→\n 547→ .image-modal img {\n 548→ display: block;\n 549→ max-width: min(1140px, 92vw);\n 550→ max-height: 84vh;\n 551→ width: auto;\n 552→ height: auto;\n 553→ object-fit: contain;\n 554→ border-radius: 10px;\n 555→ background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n 556→ }\n 557→\n 558→ .image-modal .close {\n 559→ position: absolute;\n 560→ top: 10px;\n 561→ right: 10px;\n 562→ border: 1px solid var(--line);\n 563→ background: color-mix(in srgb, var(--panel) 92%, transparent);\n 564→ color: var(--text);\n 565→ border-radius: 999px;\n 566→ padding: 8px 10px;\n 567→ font: inherit;\n 568→ cursor: pointer;\n 569→ }\n 570→\n 571→ .image-modal .close:hover { border-color: var(--line-2); }\n 572→\n 573→ .empty {\n 574→ padding: 14px;\n 575→ color: var(--muted);\n 576→ font-size: 13px;\n 577→ line-height: 1.35;\n 578→ }\n 579→\n 580→ /* Pretty view styles */\n 581→ .detail-pretty {\n 582→ padding: 14px;\n 583→ border-bottom: 1px solid var(--line);\n 584→ overflow: auto;\n 585→ max-height: 50vh;\n 586→ }\n 587→\n 588→ .detail.inline .detail-pretty {\n 589→ max-height: 40vh;\n 590→ }\n 591→\n 592→ .pretty-section {\n 593→ margin-bottom: 14px;\n 594→ }\n 595→\n 596→ .pretty-section:last-child {\n 597→ margin-bottom: 0;\n 598→ }\n 599→\n 600→ .pretty-label {\n 601→ font-size: 11px;\n 602→ font-weight: 600;\n 603→ text-transform: uppercase;\n 604→ letter-spacing: 0.5px;\n 605→ color: var(--muted);\n 606→ margin-bottom: 6px;\n 607→ }\n 608→\n 609→ .pretty-content {\n 610→ font-size: 13px;\n 611→ line-height: 1.5;\n 612→ }\n 613→\n 614→ .pretty-content p {\n 615→ margin: 0 0 10px;\n 616→ }\n 617→\n 618→ .pretty-content p:last-child {\n 619→ margin-bottom: 0;\n 620→ }\n 621→\n 622→ .pretty-content h1, .pretty-content h2, .pretty-content h3,\n 623→ .pretty-content h4, .pretty-content h5, .pretty-content h6 {\n 624→ margin: 14px 0 8px;\n 625→ font-weight: 600;\n 626→ line-height: 1.3;\n 627→ }\n 628→\n 629→ .pretty-content h1:first-child, .pretty-content h2:first-child,\n 630→ .pretty-content h3:first-child {\n 631→ margin-top: 0;\n 632→ }\n 633→\n 634→ .pretty-content h1 { font-size: 1.4em; }\n 635→ .pretty-content h2 { font-size: 1.25em; }\n 636→ .pretty-content h3 { font-size: 1.1em; }\n 637→\n 638→ .pretty-content ul, .pretty-content ol {\n 639→ margin: 8px 0;\n 640→ padding-left: 1.5em;\n 641→ list-style-position: outside;\n 642→ }\n 643→\n 644→ .pretty-content li {\n 645→ margin: 4px 0;\n 646→ }\n 647→\n 648→ .thinking-block ul, .thinking-block ol {\n 649→ padding-left: 2em;\n 650→ }\n 651→\n 652→ .pretty-content code {\n 653→ font-family: var(--mono);\n 654→ font-size: 0.9em;\n 655→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 656→ border: 1px solid var(--line);\n 657→ padding: 2px 5px;\n 658→ border-radius: 5px;\n 659→ }\n 660→\n 661→ .pretty-content pre {\n 662→ margin: 10px 0;\n 663→ padding: 12px;\n 664→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 665→ border: 1px solid var(--line);\n 666→ border-radius: 10px;\n 667→ overflow-x: auto;\n 668→ }\n 669→\n 670→ .pretty-content pre code {\n 671→ background: none;\n 672→ border: none;\n 673→ padding: 0;\n 674→ font-size: 12px;\n 675→ }\n 676→\n 677→ .pretty-content blockquote {\n 678→ margin: 10px 0;\n 679→ padding: 8px 12px;\n 680→ border-left: 3px solid var(--accent);\n 681→ background: color-mix(in srgb, var(--accent) 8%, transparent);\n 682→ border-radius: 0 8px 8px 0;\n 683→ }\n 684→\n 685→ .pretty-content a {\n 686→ color: var(--accent);\n 687→ text-decoration: none;\n 688→ }\n 689→\n 690→ .pretty-content a:hover {\n 691→ text-decoration: underline;\n 692→ }\n 693→\n 694→ .pretty-content strong {\n 695→ font-weight: 600;\n 696→ }\n 697→\n 698→ .pretty-content em {\n 699→ font-style: italic;\n 700→ }\n 701→\n 702→ .pretty-content table {\n 703→ border-collapse: collapse;\n 704→ margin: 10px 0;\n 705→ width: 100%;\n 706→ font-size: 13px;\n 707→ }\n 708→\n 709→ .pretty-content th,\n 710→ .pretty-content td {\n 711→ border: 1px solid var(--line);\n 712→ padding: 8px 10px;\n 713→ text-align: left;\n 714→ }\n 715→\n 716→ .pretty-content th {\n 717→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 718→ font-weight: 600;\n 719→ }\n 720→\n 721→ .pretty-content tr:hover {\n 722→ background: color-mix(in srgb, var(--accent) 6%, transparent);\n 723→ }\n 724→\n 725→ .command-block {\n 726→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 727→ border: 1px solid var(--line);\n 728→ border-radius: 10px;\n 729→ overflow: hidden;\n 730→ margin: 8px 0;\n 731→ }\n 732→\n 733→ .command-header {\n 734→ display: flex;\n 735→ align-items: center;\n 736→ gap: 8px;\n 737→ padding: 8px 12px;\n 738→ background: color-mix(in srgb, var(--accent) 12%, transparent);\n 739→ border-bottom: 1px solid var(--line);\n 740→ font-size: 12px;\n 741→ }\n 742→\n 743→ .command-header .tool-name {\n 744→ font-weight: 600;\n 745→ color: var(--accent);\n 746→ }\n 747→\n 748→ .command-header .tool-desc {\n 749→ color: var(--muted);\n 750→ }\n 751→\n 752→ .command-input {\n 753→ padding: 10px 12px;\n 754→ font-family: var(--mono);\n 755→ font-size: 12px;\n 756→ line-height: 1.4;\n 757→ white-space: pre-wrap;\n 758→ word-break: break-word;\n 759→ border-bottom: 1px solid var(--line);\n 760→ }\n 761→\n 762→ .command-input::before {\n 763→ content: '$ ';\n 764→ color: var(--good);\n 765→ font-weight: 600;\n 766→ }\n 767→\n 768→ .command-output {\n 769→ padding: 10px 12px;\n 770→ font-family: var(--mono);\n 771→ font-size: 12px;\n 772→ line-height: 1.4;\n 773→ white-space: pre-wrap;\n 774→ word-break: break-word;\n 775→ max-height: 300px;\n 776→ overflow: auto;\n 777→ color: var(--muted);\n 778→ }\n 779→\n 780→ .file-path {\n 781→ font-family: var(--mono);\n 782→ font-size: 12px;\n 783→ padding: 6px 10px;\n 784→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 785→ border: 1px solid var(--line);\n 786→ border-radius: 8px;\n 787→ display: inline-block;\n 788→ margin: 4px 0;\n 789→ }\n 790→\n 791→ .tool-params {\n 792→ display: grid;\n 793→ gap: 8px;\n 794→ margin-top: 8px;\n 795→ }\n 796→\n 797→ .tool-param {\n 798→ display: grid;\n 799→ grid-template-columns: auto 1fr;\n 800→ gap: 8px;\n 801→ align-items: start;\n 802→ font-size: 12px;\n 803→ }\n 804→\n 805→ .tool-param-name {\n 806→ font-family: var(--mono);\n 807→ font-weight: 600;\n 808→ color: var(--accent);\n 809→ padding: 2px 6px;\n 810→ background: color-mix(in srgb, var(--accent) 12%, transparent);\n 811→ border-radius: 4px;\n 812→ }\n 813→\n 814→ .tool-param-value {\n 815→ font-family: var(--mono);\n 816→ white-space: pre-wrap;\n 817→ word-break: break-word;\n 818→ padding: 2px 0;\n 819→ }\n 820→\n 821→ .thinking-block {\n 822→ background: color-mix(in srgb, var(--accent-2) 10%, transparent);\n 823→ border: 1px solid color-mix(in srgb, var(--accent-2) 30%, var(--line));\n 824→ border-radius: 10px;\n 825→ padding: 12px 16px;\n 826→ font-size: 13px;\n 827→ line-height: 1.5;\n 828→ }\n 829→\n 830→ .result-block {\n 831→ background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n 832→ border: 1px solid var(--line);\n 833→ border-radius: 10px;\n 834→ padding: 12px;\n 835→ font-family: var(--mono);\n 836→ font-size: 12px;\n 837→ line-height: 1.4;\n 838→ white-space: pre-wrap;\n 839→ word-break: break-word;\n 840→ max-height: 400px;\n 841→ overflow: auto;\n 842→ }\n 843→\n 844→ .result-error {\n 845→ border-color: color-mix(in srgb, var(--bad) 50%, var(--line));\n 846→ background: color-mix(in srgb, var(--bad) 8%, transparent);\n 847→ }\n 848→\n 849→ .json-toggle {\n 850→ display: flex;\n 851→ align-items: center;\n 852→ justify-content: space-between;\n 853→ padding: 8px 14px;\n 854→ border-bottom: 1px solid var(--line);\n 855→ background: color-mix(in srgb, var(--panel-2) 50%, transparent);\n 856→ }\n 857→\n 858→ .json-toggle-label {\n 859→ font-size: 12px;\n 860→ color: var(--muted);\n 861→ font-weight: 500;\n 862→ }\n 863→\n 864→ .prompts-modal {\n 865→ position: fixed;\n 866→ inset: 0;\n 867→ z-index: 80;\n 868→ background: rgba(0, 0, 0, 0.68);\n 869→ display: none;\n 870→ padding: 18px;\n 871→ place-items: center;\n 872→ }\n 873→\n 874→ .prompts-modal.open { display: grid; }\n 875→\n 876→ .prompts-modal .dialog {\n 877→ position: relative;\n 878→ border-radius: 16px;\n 879→ border: 1px solid var(--line);\n 880→ background: color-mix(in srgb, var(--panel) 98%, transparent);\n 881→ box-shadow: var(--shadow);\n 882→ padding: 14px;\n 883→ width: min(1100px, 96vw);\n 884→ height: min(86vh, 780px);\n 885→ display: grid;\n 886→ grid-template-rows: auto 1fr auto;\n 887→ gap: 10px;\n 888→ }\n 889→\n 890→ .prompts-head {\n 891→ display: flex;\n 892→ gap: 10px;\n 893→ align-items: center;\n 894→ justify-content: space-between;\n 895→ }\n 896→\n 897→ .prompts-title {\n 898→ font-weight: 700;\n 899→ letter-spacing: 0.2px;\n 900→ }\n 901→\n 902→ .prompts-modal textarea {\n 903→ width: 100%;\n 904→ height: 100%;\n 905→ resize: none;\n 906→ border-radius: 12px;\n 907→ font-family: var(--mono);\n 908→ font-size: 12px;\n 909→ line-height: 1.45;\n 910→ padding: 12px;\n 911→ }\n 912→\n 913→ .prompts-actions {\n 914→ display: flex;\n 915→ gap: 10px;\n 916→ align-items: center;\n 917→ justify-content: flex-end;\n 918→ flex-wrap: wrap;\n 919→ }\n 920→ </style>\n 921→</head>\n 922→ <body>\n 923→ <div class=\"wrap\">\n 924→ <header>\n 925→ <div>\n 926→ <h1>Claude Code Timeline Viewer</h1>\n 927→ <p class=\"sub\">Open a Claude Code session <code>.jsonl</code> file and explore it as an interactive timeline. Times are shown in your local timezone by default. Find sessions in <code>~/.claude/projects/</code>.</p>\n 928→ </div>\n 929→ </header>\n 930→\n 931→ <section class=\"panel input-panel\" aria-label=\"Input\">\n 932→ <div class=\"input-grid\">\n 933→ <div>\n 934→ <div id=\"dropzone\" class=\"dropzone\" tabindex=\"0\">\n 935→ <strong>Drop a <code>.jsonl</code> file here</strong>\n 936→ <div class=\"muted\">or click to choose a file</div>\n 937→ <div class=\"tiny\">Supported: file picker, drag-and-drop, paste, or URL fetch (CORS-enabled).</div>\n 938→ <input id=\"file-input\" type=\"file\" accept=\".jsonl,application/json,text/plain\" hidden />\n 939→ </div>\n 940→ </div>\n 941→ <div>\n 942→ <div class=\"tabs\" role=\"tablist\" aria-label=\"Input tabs\">\n 943→ <button type=\"button\" class=\"tab active\" data-tab=\"paste\" aria-selected=\"true\">Paste</button>\n 944→ <button type=\"button\" class=\"tab\" data-tab=\"url\" aria-selected=\"false\">URL</button>\n 945→ </div>\n 946→\n 947→ <div id=\"panel-paste\" data-panel=\"paste\">\n 948→ <textarea id=\"paste-input\" placeholder=\"Paste JSONL content here...\"></textarea>\n 949→ <div class=\"row\">\n 950→ <button type=\"button\" class=\"btn primary\" id=\"render-paste\">Render pasted content</button>\n 951→ <div class=\"tiny\">Tip: the timeline uses one JSON object per line.</div>\n 952→ </div>\n 953→ </div>\n 954→\n 955→ <div id=\"panel-url\" data-panel=\"url\" hidden>\n 956→ <input id=\"url-input\" type=\"url\" placeholder=\"https://example.com/session.jsonl\" />\n 957→ <div class=\"row\">\n 958→ <button type=\"button\" class=\"btn primary\" id=\"fetch-url\">Fetch and render</button>\n 959→ <div class=\"tiny\">The URL must allow cross-origin requests (CORS).</div>\n 960→ </div>\n 961→ </div>\n 962→ </div>\n 963→ </div>\n 964→\n 965→ <div id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></div>\n 966→ </section>\n 967→\n 968→ <section class=\"panel filters-panel\" aria-label=\"Filters\">\n 969→ <div class=\"controls\" aria-label=\"Timeline controls\">\n 970→ <div class=\"field\" style=\"min-width: 170px\">\n 971→ <div class=\"label\">Timezone</div>\n 972→ <div class=\"segmented\" role=\"group\" aria-label=\"Timezone toggle\">\n 973→ <button type=\"button\" id=\"tz-local\" class=\"active\">Local</button>\n 974→ <button type=\"button\" id=\"tz-utc\">UTC</button>\n 975→ </div>\n 976→ </div>\n 977→ <div class=\"field\" style=\"min-width: 220px\">\n 978→ <div class=\"label\">Search</div>\n 979→ <input id=\"search\" type=\"search\" placeholder=\"message, tool name, cwd...\" autocomplete=\"off\" />\n 980→ </div>\n 981→ <div class=\"field\" style=\"min-width: 190px\">\n 982→ <div class=\"label\">Type</div>\n 983→ <select id=\"type-filter\"></select>\n 984→ </div>\n 985→ <div class=\"field\" style=\"min-width: 210px\">\n 986→ <div class=\"label\">Content Type</div>\n 987→ <select id=\"content-type-filter\"></select>\n 988→ </div>\n 989→ <div class=\"field\" style=\"min-width: 160px\">\n 990→ <div class=\"label\">Role</div>\n 991→ <select id=\"role-filter\"></select>\n 992→ </div>\n 993→ <label class=\"check\" title=\"Hide thinking blocks (can be verbose)\">\n 994→ <input type=\"checkbox\" id=\"hide-thinking\" />\n 995→ Hide thinking\n 996→ </label>\n 997→ <button type=\"button\" class=\"btn\" id=\"clear-filters\">Clear</button>\n 998→ </div>\n 999→ </section>\n 1000→\n 1001→ <div class=\"controls\" style=\"margin-bottom: 14px;\">\n 1002→ <button type=\"button\" class=\"btn\" id=\"show-prompts\" disabled>Show prompts</button>\n 1003→ </div>\n 1004→\n 1005→ <main>\n 1006→ <section class=\"panel timeline-panel\" aria-label=\"Timeline\">\n 1007→ <div class=\"stats\">\n 1008→ <div class=\"big\"><span id=\"stats-showing\">0</span> <span class=\"muted\">showing</span> <span class=\"muted\">/</span> <span id=\"stats-total\">0</span> <span class=\"muted\">events</span></div>\n 1009→ <div class=\"muted\" id=\"stats-range\">-</div>\n 1010→ </div>\n 1011→ <div id=\"timeline\" class=\"timeline\" role=\"list\" tabindex=\"0\" aria-label=\"Events list\"></div>\n 1012→ </section>\n 1013→\n 1014→ <aside class=\"panel detail\" aria-label=\"Details\">\n 1015→ <div class=\"detail-head\">\n 1016→ <div class=\"detail-title\" id=\"detail-title\">Select an event</div>\n 1017→ <div class=\"detail-actions\">\n 1018→ <button type=\"button\" class=\"btn\" id=\"copy-json\" disabled>Copy JSON</button>\n 1019→ <button type=\"button\" class=\"btn\" id=\"copy-raw\" disabled>Copy raw line</button>\n 1020→ <label class=\"check\" title=\"Truncate very long strings in the JSON preview\">\n 1021→ <input type=\"checkbox\" id=\"truncate-strings\" checked />\n 1022→ Truncate long strings\n 1023→ </label>\n 1024→ </div>\n 1025→ </div>\n 1026→ <div id=\"detail-images\" class=\"detail-images\" hidden aria-label=\"Event images\"></div>\n 1027→ <div id=\"detail-pretty\" class=\"detail-pretty\" hidden aria-label=\"Formatted event view\"></div>\n 1028→ <div class=\"json-toggle\">\n 1029→ <span class=\"json-toggle-label\">Raw JSON</span>\n 1030→ </div>\n 1031→ <pre id=\"detail-json\" aria-label=\"Selected event JSON\" class=\"empty\">Load a file to get started.</pre>\n 1032→ </aside>\n 1033→ </main>\n 1034→ </div>\n 1035→\n 1036→ <div id=\"image-modal\" class=\"image-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\">\n 1037→ <div class=\"dialog\">\n 1038→ <button type=\"button\" class=\"close\" id=\"image-modal-close\" aria-label=\"Close image\">Close</button>\n 1039→ <img id=\"image-modal-img\" alt=\"\" />\n 1040→ </div>\n 1041→ </div>\n 1042→\n 1043→ <div id=\"prompts-modal\" class=\"prompts-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\" aria-label=\"User prompts\">\n 1044→ <div class=\"dialog\">\n 1045→ <div class=\"prompts-head\">\n 1046→ <div class=\"prompts-title\">User prompts</div>\n 1047→ <button type=\"button\" class=\"btn\" id=\"prompts-close\" aria-label=\"Close prompts\">Close</button>\n 1048→ </div>\n 1049→ <textarea id=\"prompts-text\" readonly spellcheck=\"false\"></textarea>\n 1050→ <div class=\"prompts-actions\">\n 1051→ <button type=\"button\" class=\"btn primary\" id=\"prompts-copy\">Copy</button>\n 1052→ </div>\n 1053→ </div>\n 1054→ </div>\n 1055→\n 1056→ <script>\n 1057→ const els = {\n 1058→ dropzone: document.getElementById('dropzone'),\n 1059→ fileInput: document.getElementById('file-input'),\n 1060→ pasteInput: document.getElementById('paste-input'),\n 1061→ renderPaste: document.getElementById('render-paste'),\n 1062→ urlInput: document.getElementById('url-input'),\n 1063→ fetchUrl: document.getElementById('fetch-url'),\n 1064→ status: document.getElementById('status'),\n 1065→ timeline: document.getElementById('timeline'),\n 1066→ detailPanel: document.querySelector('aside.detail'),\n 1067→ detailTitle: document.getElementById('detail-title'),\n 1068→ detailImages: document.getElementById('detail-images'),\n 1069→ detailPretty: document.getElementById('detail-pretty'),\n 1070→ detailJson: document.getElementById('detail-json'),\n 1071→ copyJson: document.getElementById('copy-json'),\n 1072→ copyRaw: document.getElementById('copy-raw'),\n 1073→ truncateStrings: document.getElementById('truncate-strings'),\n 1074→ tzLocal: document.getElementById('tz-local'),\n 1075→ tzUtc: document.getElementById('tz-utc'),\n 1076→ search: document.getElementById('search'),\n 1077→ typeFilter: document.getElementById('type-filter'),\n 1078→ contentTypeFilter: document.getElementById('content-type-filter'),\n 1079→ roleFilter: document.getElementById('role-filter'),\n 1080→ hideThinking: document.getElementById('hide-thinking'),\n 1081→ statsShowing: document.getElementById('stats-showing'),\n 1082→ statsTotal: document.getElementById('stats-total'),\n 1083→ statsRange: document.getElementById('stats-range'),\n 1084→ clearFilters: document.getElementById('clear-filters'),\n 1085→ imageModal: document.getElementById('image-modal'),\n 1086→ imageModalImg: document.getElementById('image-modal-img'),\n 1087→ imageModalClose: document.getElementById('image-modal-close'),\n 1088→ showPrompts: document.getElementById('show-prompts'),\n 1089→ promptsModal: document.getElementById('prompts-modal'),\n 1090→ promptsClose: document.getElementById('prompts-close'),\n 1091→ promptsText: document.getElementById('prompts-text'),\n 1092→ promptsCopy: document.getElementById('prompts-copy'),\n 1093→ };\n 1094→\n 1095→ const state = {\n 1096→ sourceLabel: null,\n 1097→ tzMode: 'local',\n 1098→ events: [],\n 1099→ filtered: [],\n 1100→ selectedId: null,\n 1101→ selected: null,\n 1102→ filters: {\n 1103→ query: '',\n 1104→ type: 'all',\n 1105→ contentType: 'all',\n 1106→ role: 'all',\n 1107→ hideThinking: false,\n 1108→ },\n 1109→ };\n 1110→\n 1111→ let lastActiveElementBeforeModal = null;\n 1112→ let lastBodyOverflow = null;\n 1113→ let lastActiveElementBeforePrompts = null;\n 1114→ let lastBodyOverflowBeforePrompts = null;\n 1115→ let promptsCopiedTimer = null;\n 1116→ let suppressUrlStateSync = false;\n 1117→ const wideLayoutMq = window.matchMedia('(min-width: 1060px)');\n 1118→ const detailDock = document.createElement('div');\n 1119→ detailDock.id = 'detail-dock';\n 1120→ detailDock.style.display = 'none';\n 1121→ els.detailPanel.parentElement.insertBefore(detailDock, els.detailPanel);\n 1122→\n 1123→ function updateDetailPlacement() {\n 1124→ const inline = !wideLayoutMq.matches;\n 1125→\n 1126→ if (!inline) {\n 1127→ if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n 1128→ els.detailPanel.classList.remove('inline');\n 1129→ return;\n 1130→ }\n 1131→\n 1132→ const selectedNode = els.timeline.querySelector('.event.selected');\n 1133→ if (!selectedNode) {\n 1134→ if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n 1135→ els.detailPanel.classList.remove('inline');\n 1136→ return;\n 1137→ }\n 1138→\n 1139→ if (selectedNode.nextSibling !== els.detailPanel) selectedNode.after(els.detailPanel);\n 1140→ els.detailPanel.classList.add('inline');\n 1141→ }\n 1142→\n 1143→ function setStatus(message, kind = 'good') {\n 1144→ if (!message) {\n 1145→ els.status.className = 'status';\n 1146→ els.status.textContent = '';\n 1147→ return;\n 1148→ }\n 1149→ els.status.className = `status visible ${kind}`;\n 1150→ els.status.textContent = message;\n 1151→ }\n 1152→\n 1153→ function pad2(number) {\n 1154→ return String(number).padStart(2, '0');\n 1155→ }\n 1156→\n 1157→ function pad3(number) {\n 1158→ return String(number).padStart(3, '0');\n 1159→ }\n 1160→\n 1161→ function getOffsetString(date) {\n 1162→ const offsetMinutes = -date.getTimezoneOffset();\n 1163→ const sign = offsetMinutes >= 0 ? '+' : '-';\n 1164→ const abs = Math.abs(offsetMinutes);\n 1165→ const hours = Math.floor(abs / 60);\n 1166→ const minutes = abs % 60;\n 1167→ return `${sign}${pad2(hours)}:${pad2(minutes)}`;\n 1168→ }\n 1169→\n 1170→ function formatTimestamp(date, tzMode) {\n 1171→ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n 1172→ if (tzMode === 'utc') {\n 1173→ return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +\n 1174→ `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}.` +\n 1175→ `${pad3(date.getUTCMilliseconds())}Z`;\n 1176→ }\n 1177→ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ` +\n 1178→ `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}.` +\n 1179→ `${pad3(date.getMilliseconds())} ${getOffsetString(date)}`;\n 1180→ }\n 1181→\n 1182→ function dayKey(date, tzMode) {\n 1183→ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n 1184→ if (tzMode === 'utc') {\n 1185→ return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;\n 1186→ }\n 1187→ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n 1188→ }\n 1189→\n 1190→ function formatDelta(ms) {\n 1191→ if (typeof ms !== 'number' || !Number.isFinite(ms)) return '';\n 1192→ const abs = Math.abs(ms);\n 1193→ const sign = ms < 0 ? '-' : '+';\n 1194→ if (abs < 1000) return `${sign}${Math.round(abs)}ms`;\n 1195→ if (abs < 60_000) return `${sign}${(abs / 1000).toFixed(abs < 10_000 ? 2 : 1)}s`;\n 1196→ if (abs < 3_600_000) {\n 1197→ const m = Math.floor(abs / 60_000);\n 1198→ const s = Math.floor((abs % 60_000) / 1000);\n 1199→ return `${sign}${m}m ${pad2(s)}s`;\n 1200→ }\n 1201→ if (abs < 86_400_000) {\n 1202→ const h = Math.floor(abs / 3_600_000);\n 1203→ const m = Math.floor((abs % 3_600_000) / 60_000);\n 1204→ return `${sign}${h}h ${m}m`;\n 1205→ }\n 1206→ const d = Math.floor(abs / 86_400_000);\n 1207→ const h = Math.floor((abs % 86_400_000) / 3_600_000);\n 1208→ return `${sign}${d}d ${h}h`;\n 1209→ }\n 1210→\n 1211→ function safeJsonParse(text) {\n 1212→ try {\n 1213→ return { ok: true, value: JSON.parse(text) };\n 1214→ } catch (error) {\n 1215→ return { ok: false, error };\n 1216→ }\n 1217→ }\n 1218→\n 1219→ function extractMessageText(message) {\n 1220→ if (!message || typeof message !== 'object') return null;\n 1221→ const content = message.content;\n 1222→ if (typeof content === 'string') return content;\n 1223→ if (!Array.isArray(content)) return null;\n 1224→ const parts = [];\n 1225→ for (const item of content) {\n 1226→ if (!item || typeof item !== 'object') continue;\n 1227→ if (typeof item.text === 'string') parts.push(item.text);\n 1228→ else if (typeof item.thinking === 'string') parts.push(item.thinking);\n 1229→ }\n 1230→ const joined = parts.join('');\n 1231→ return joined || null;\n 1232→ }\n 1233→\n 1234→ function getContentTypes(message) {\n 1235→ if (!message || typeof message !== 'object') return [];\n 1236→ const content = message.content;\n 1237→ if (typeof content === 'string') return ['text'];\n 1238→ if (!Array.isArray(content)) return [];\n 1239→ const types = new Set();\n 1240→ for (const item of content) {\n 1241→ if (!item || typeof item !== 'object') continue;\n 1242→ if (item.type) types.add(item.type);\n 1243→ }\n 1244→ return Array.from(types);\n 1245→ }\n 1246→\n 1247→ function firstLine(text) {\n 1248→ if (typeof text !== 'string') return null;\n 1249→ const trimmed = text.trim();\n 1250→ if (!trimmed) return null;\n 1251→ const idx = trimmed.indexOf('\\n');\n 1252→ return idx === -1 ? trimmed : trimmed.slice(0, idx).trim();\n 1253→ }\n 1254→\n 1255→ function normalizeSummaryText(text, maxLen = 260) {\n 1256→ if (typeof text !== 'string') return '';\n 1257→ const singleLine = text.replace(/\\s+/g, ' ').trim();\n 1258→ if (singleLine.length <= maxLen) return singleLine;\n 1259→ return singleLine.slice(0, maxLen - 1) + '...';\n 1260→ }\n 1261→\n 1262→ function summarizeEvent(obj) {\n 1263→ const kind = obj?.type || '';\n 1264→ const message = obj?.message;\n 1265→\n 1266→ if (kind === 'file-history-snapshot') {\n 1267→ const msgId = obj?.messageId ? `msg=${obj.messageId.slice(0, 8)}...` : null;\n 1268→ return normalizeSummaryText(msgId || 'file-history-snapshot');\n 1269→ }\n 1270→\n 1271→ if (kind === 'user' || kind === 'assistant') {\n 1272→ const role = message?.role || kind;\n 1273→ const content = message?.content;\n 1274→\n 1275→ if (typeof content === 'string') {\n 1276→ return normalizeSummaryText(`${role}: ${content}`);\n 1277→ }\n 1278→\n 1279→ if (Array.isArray(content) && content.length > 0) {\n 1280→ for (const item of content) {\n 1281→ if (item?.type === 'text' && item.text) {\n 1282→ return normalizeSummaryText(`${role}: ${item.text}`);\n 1283→ }\n 1284→ if (item?.type === 'thinking' && item.thinking) {\n 1285→ return normalizeSummaryText(`${role} (thinking): ${item.thinking}`);\n 1286→ }\n 1287→ if (item?.type === 'tool_use') {\n 1288→ const name = item.name || 'tool';\n 1289→ const input = item.input;\n 1290→ if (name === 'Bash' && input?.command) {\n 1291→ return normalizeSummaryText(`${name}: ${input.command}`);\n 1292→ }\n 1293→ if (name === 'Read' && input?.file_path) {\n 1294→ return normalizeSummaryText(`${name}: ${input.file_path}`);\n 1295→ }\n 1296→ if (name === 'Write' && input?.file_path) {\n 1297→ return normalizeSummaryText(`${name}: ${input.file_path}`);\n 1298→ }\n 1299→ if (name === 'Edit' && input?.file_path) {\n 1300→ return normalizeSummaryText(`${name}: ${input.file_path}`);\n 1301→ }\n 1302→ if (name === 'Glob' && input?.pattern) {\n 1303→ return normalizeSummaryText(`${name}: ${input.pattern}`);\n 1304→ }\n 1305→ if (name === 'Grep' && input?.pattern) {\n 1306→ return normalizeSummaryText(`${name}: ${input.pattern}`);\n 1307→ }\n 1308→ if (name === 'Task' && input?.description) {\n 1309→ return normalizeSummaryText(`${name}: ${input.description}`);\n 1310→ }\n 1311→ if (input && typeof input === 'object') {\n 1312→ const keys = Object.keys(input).slice(0, 3);\n 1313→ return normalizeSummaryText(`${name}(${keys.join(', ')}${Object.keys(input).length > 3 ? ', ...' : ''})`);\n 1314→ }\n 1315→ return normalizeSummaryText(`${name}()`);\n 1316→ }\n 1317→ if (item?.type === 'tool_result') {\n 1318→ const toolUseId = item.tool_use_id ? item.tool_use_id.slice(0, 12) : 'result';\n 1319→ const resultContent = item.content;\n 1320→ if (typeof resultContent === 'string') {\n 1321→ return normalizeSummaryText(`tool_result: ${firstLine(resultContent) || toolUseId}`);\n 1322→ }\n 1323→ if (Array.isArray(resultContent)) {\n 1324→ for (const rc of resultContent) {\n 1325→ if (rc?.type === 'text' && rc.text) {\n 1326→ return normalizeSummaryText(`tool_result: ${firstLine(rc.text) || toolUseId}`);\n 1327→ }\n 1328→ if (rc?.type === 'image') {\n 1329→ return normalizeSummaryText(`tool_result: [image]`);\n 1330→ }\n 1331→ }\n 1332→ }\n 1333→ return normalizeSummaryText(`tool_result: ${toolUseId}`);\n 1334→ }\n 1335→ if (item?.type === 'image') {\n 1336→ return normalizeSummaryText(`${role}: [image]`);\n 1337→ }\n 1338→ }\n 1339→ return normalizeSummaryText(`${role}: [${content.length} content items]`);\n 1340→ }\n 1341→\n 1342→ return kind;\n 1343→ }\n 1344→\n 1345→ return kind || 'event';\n 1346→ }\n 1347→\n 1348→ function buildSearchText(obj, summary) {\n 1349→ const pieces = [];\n 1350→ pieces.push(obj?.type || '');\n 1351→ pieces.push(obj?.uuid || '');\n 1352→ pieces.push(obj?.sessionId || '');\n 1353→ pieces.push(obj?.cwd || '');\n 1354→ pieces.push(obj?.gitBranch || '');\n 1355→ pieces.push(obj?.slug || '');\n 1356→ const message = obj?.message;\n 1357→ if (message && typeof message === 'object') {\n 1358→ pieces.push(message.role || '');\n 1359→ const msg = extractMessageText(message);\n 1360→ if (msg) pieces.push(msg);\n 1361→ const content = message.content;\n 1362→ if (Array.isArray(content)) {\n 1363→ for (const item of content) {\n 1364→ if (item?.type === 'tool_use') {\n 1365→ pieces.push(item.name || '');\n 1366→ if (item.input && typeof item.input === 'object') {\n 1367→ pieces.push(JSON.stringify(item.input).slice(0, 500));\n 1368→ }\n 1369→ }\n 1370→ if (item?.type === 'tool_result' && typeof item.content === 'string') {\n 1371→ pieces.push(item.content.slice(0, 500));\n 1372→ }\n 1373→ }\n 1374→ }\n 1375→ }\n 1376→ if (typeof summary === 'string') pieces.push(summary);\n 1377→ return pieces.join('\\n').toLowerCase();\n 1378→ }\n 1379→\n 1380→ function stringifyJson(value, truncateLongStrings) {\n 1381→ const maxLen = 8000;\n 1382→ return JSON.stringify(value, (key, v) => {\n 1383→ if (!truncateLongStrings) return v;\n 1384→ if (typeof v === 'string' && v.length > maxLen) {\n 1385→ return v.slice(0, maxLen) + `... [truncated ${v.length - maxLen} chars]`;\n 1386→ }\n 1387→ return v;\n 1388→ }, 2);\n 1389→ }\n 1390→\n 1391→ function updateFilterOptions(events) {\n 1392→ const typeSet = new Set();\n 1393→ const contentTypeSet = new Set();\n 1394→ const roleSet = new Set();\n 1395→ for (const e of events) {\n 1396→ if (e.kind) typeSet.add(e.kind);\n 1397→ for (const ct of e.contentTypes) {\n 1398→ contentTypeSet.add(ct);\n 1399→ }\n 1400→ if (!e.contentTypes.length) contentTypeSet.add('(none)');\n 1401→ if (e.role) roleSet.add(e.role);\n 1402→ }\n 1403→\n 1404→ function setOptions(select, items, current) {\n 1405→ select.innerHTML = '';\n 1406→ const optAll = document.createElement('option');\n 1407→ optAll.value = 'all';\n 1408→ optAll.textContent = 'All';\n 1409→ select.appendChild(optAll);\n 1410→ for (const item of items) {\n 1411→ const opt = document.createElement('option');\n 1412→ opt.value = item;\n 1413→ opt.textContent = item;\n 1414→ select.appendChild(opt);\n 1415→ }\n 1416→ select.value = current;\n 1417→ }\n 1418→\n 1419→ setOptions(els.typeFilter, Array.from(typeSet).sort(), state.filters.type);\n 1420→ setOptions(els.contentTypeFilter, Array.from(contentTypeSet).sort(), state.filters.contentType);\n 1421→ setOptions(els.roleFilter, Array.from(roleSet).sort(), state.filters.role);\n 1422→\n 1423→ if (events.length) {\n 1424→ state.filters.type = els.typeFilter.value || 'all';\n 1425→ state.filters.contentType = els.contentTypeFilter.value || 'all';\n 1426→ state.filters.role = els.roleFilter.value || 'all';\n 1427→ }\n 1428→ }\n 1429→\n 1430→ function readHashParams() {\n 1431→ const raw = window.location.hash && window.location.hash.startsWith('#')\n 1432→ ? window.location.hash.slice(1)\n 1433→ : '';\n 1434→ return new URLSearchParams(raw);\n 1435→ }\n 1436→\n 1437→ function writeHashParams(params) {\n 1438→ const base = window.location.href.split('#')[0];\n 1439→ const hash = params.toString();\n 1440→ const next = base + (hash ? `#${hash}` : '');\n 1441→ window.history.replaceState(null, '', next);\n 1442→ }\n 1443→\n 1444→ function syncUrlHashFromControls({ clearSel = false } = {}) {\n 1445→ if (suppressUrlStateSync) return;\n 1446→ const params = readHashParams();\n 1447→ params.set('tz', state.tzMode);\n 1448→ params.set('q', (els.search.value || '').trim());\n 1449→ params.set('type', state.filters.type || 'all');\n 1450→ params.set('ct', state.filters.contentType || 'all');\n 1451→ params.set('role', state.filters.role || 'all');\n 1452→ params.set('hide', state.filters.hideThinking ? '1' : '0');\n 1453→ params.set('truncate', els.truncateStrings.checked ? '1' : '0');\n 1454→ const selectedLine = state.selected?.line;\n 1455→ if (typeof selectedLine === 'number' && Number.isFinite(selectedLine)) {\n 1456→ params.set('sel', String(selectedLine));\n 1457→ } else if (clearSel) {\n 1458→ params.delete('sel');\n 1459→ }\n 1460→ writeHashParams(params);\n 1461→ }\n 1462→\n 1463→ function applySelectionFromHash() {\n 1464→ if (!state.events.length) return;\n 1465→ const params = readHashParams();\n 1466→ const raw = params.get('sel');\n 1467→ if (!raw) return;\n 1468→ const line = Number(raw);\n 1469→ if (!Number.isFinite(line)) return;\n 1470→ const found = state.events.find(e => e.line === line);\n 1471→ if (!found) return;\n 1472→ if (state.selectedId === found.id) return;\n 1473→ suppressUrlStateSync = true;\n 1474→ try {\n 1475→ selectEvent(found.id, { scroll: true });\n 1476→ } finally {\n 1477→ suppressUrlStateSync = false;\n 1478→ }\n 1479→ }\n 1480→\n 1481→ function applyControlsFromHash() {\n 1482→ const params = readHashParams();\n 1483→ suppressUrlStateSync = true;\n 1484→ try {\n 1485→ const q = params.get('q');\n 1486→ if (q !== null) {\n 1487→ els.search.value = q;\n 1488→ state.filters.query = q.trim().toLowerCase();\n 1489→ }\n 1490→\n 1491→ const type = params.get('type');\n 1492→ if (type !== null) state.filters.type = type;\n 1493→ const ct = params.get('ct');\n 1494→ if (ct !== null) state.filters.contentType = ct;\n 1495→ const role = params.get('role');\n 1496→ if (role !== null) state.filters.role = role;\n 1497→\n 1498→ const hide = params.get('hide');\n 1499→ if (hide !== null) {\n 1500→ els.hideThinking.checked = hide === '1' || hide === 'true';\n 1501→ state.filters.hideThinking = els.hideThinking.checked;\n 1502→ }\n 1503→\n 1504→ const truncate = params.get('truncate');\n 1505→ if (truncate !== null) {\n 1506→ els.truncateStrings.checked = truncate === '1' || truncate === 'true';\n 1507→ }\n 1508→\n 1509→ const tz = params.get('tz');\n 1510→ if (tz === 'utc' || tz === 'local') {\n 1511→ state.tzMode = tz;\n 1512→ els.tzLocal.classList.toggle('active', tz === 'local');\n 1513→ els.tzUtc.classList.toggle('active', tz === 'utc');\n 1514→ }\n 1515→ } finally {\n 1516→ suppressUrlStateSync = false;\n 1517→ }\n 1518→ }\n 1519→\n 1520→ function applyFilters() {\n 1521→ const query = state.filters.query;\n 1522→ const type = state.filters.type;\n 1523→ const contentType = state.filters.contentType;\n 1524→ const role = state.filters.role;\n 1525→ const hideThinking = state.filters.hideThinking;\n 1526→\n 1527→ const out = [];\n 1528→ for (const e of state.events) {\n 1529→ if (hideThinking && e.contentTypes.includes('thinking') && e.contentTypes.length === 1) continue;\n 1530→ if (type !== 'all' && e.kind !== type) continue;\n 1531→ if (contentType !== 'all') {\n 1532→ const ct = e.contentTypes.length ? e.contentTypes : ['(none)'];\n 1533→ if (!ct.includes(contentType)) continue;\n 1534→ }\n 1535→ if (role !== 'all' && e.role !== role) continue;\n 1536→ if (query && !e.search.includes(query)) continue;\n 1537→ out.push(e);\n 1538→ }\n 1539→\n 1540→ state.filtered = out;\n 1541→ els.statsShowing.textContent = String(out.length);\n 1542→ els.statsTotal.textContent = String(state.events.length);\n 1543→\n 1544→ const start = out.find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n 1545→ const end = [...out].reverse().find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n 1546→ if (start && end) {\n 1547→ els.statsRange.textContent = `${formatTimestamp(start, state.tzMode)} -> ${formatTimestamp(end, state.tzMode)}`;\n 1548→ } else {\n 1549→ els.statsRange.textContent = '-';\n 1550→ }\n 1551→\n 1552→ renderTimeline();\n 1553→ }\n 1554→\n 1555→ function clearSelection({ syncHash = true } = {}) {\n 1556→ state.selectedId = null;\n 1557→ state.selected = null;\n 1558→ els.detailTitle.textContent = 'Select an event';\n 1559→ renderDetailImages(null);\n 1560→ renderPrettyView(null);\n 1561→ els.detailJson.textContent = state.events.length ? 'Select an event from the timeline.' : 'Load a file to get started.';\n 1562→ els.detailJson.classList.add('empty');\n 1563→ els.copyJson.disabled = true;\n 1564→ els.copyRaw.disabled = true;\n 1565→ for (const node of els.timeline.querySelectorAll('.event.selected')) {\n 1566→ node.classList.remove('selected');\n 1567→ }\n 1568→ updateDetailPlacement();\n 1569→ if (syncHash) syncUrlHashFromControls({ clearSel: true });\n 1570→ }\n 1571→\n 1572→ function renderTimeline() {\n 1573→ els.timeline.innerHTML = '';\n 1574→\n 1575→ if (!state.filtered.length) {\n 1576→ const empty = document.createElement('div');\n 1577→ empty.className = 'empty';\n 1578→ empty.textContent = state.events.length ? 'No events match your filters.' : 'Load a file to see a timeline.';\n 1579→ els.timeline.appendChild(empty);\n 1580→ clearSelection();\n 1581→ return;\n 1582→ }\n 1583→\n 1584→ const fragment = document.createDocumentFragment();\n 1585→ let lastDay = null;\n 1586→ let prevDate = null;\n 1587→\n 1588→ for (const e of state.filtered) {\n 1589→ const day = dayKey(e.date, state.tzMode);\n 1590→ if (day !== lastDay) {\n 1591→ lastDay = day;\n 1592→ const sep = document.createElement('div');\n 1593→ sep.className = 'day-sep';\n 1594→ sep.textContent = day;\n 1595→ fragment.appendChild(sep);\n 1596→ }\n 1597→\n 1598→ const row = document.createElement('div');\n 1599→ row.className = 'event';\n 1600→ row.setAttribute('role', 'listitem');\n 1601→ row.dataset.eventId = String(e.id);\n 1602→\n 1603→ const time = document.createElement('div');\n 1604→ time.className = 'time';\n 1605→\n 1606→ const ts = document.createElement('div');\n 1607→ ts.className = 'ts';\n 1608→ ts.textContent = formatTimestamp(e.date, state.tzMode);\n 1609→ ts.dataset.kind = 'timestamp';\n 1610→\n 1611→ const meta = document.createElement('div');\n 1612→ meta.textContent = [\n 1613→ `#${e.line}`,\n 1614→ (prevDate && e.date) ? formatDelta(e.date.getTime() - prevDate.getTime()) : null,\n 1615→ ].filter(Boolean).join(' . ');\n 1616→\n 1617→ time.appendChild(ts);\n 1618→ time.appendChild(meta);\n 1619→\n 1620→ const body = document.createElement('div');\n 1621→\n 1622→ const badges = document.createElement('div');\n 1623→ badges.className = 'badges';\n 1624→ const badgeKind = document.createElement('span');\n 1625→ badgeKind.className = `badge k-${e.kind}`;\n 1626→ badgeKind.textContent = e.kind;\n 1627→ badges.appendChild(badgeKind);\n 1628→\n 1629→ for (const ct of e.contentTypes.slice(0, 2)) {\n 1630→ const badgeCt = document.createElement('span');\n 1631→ badgeCt.className = `badge ct-${ct}`;\n 1632→ badgeCt.textContent = ct;\n 1633→ badges.appendChild(badgeCt);\n 1634→ }\n 1635→ if (e.contentTypes.length > 2) {\n 1636→ const badgeMore = document.createElement('span');\n 1637→ badgeMore.className = 'badge';\n 1638→ badgeMore.textContent = `+${e.contentTypes.length - 2}`;\n 1639→ badges.appendChild(badgeMore);\n 1640→ }\n 1641→\n 1642→ if (e.role) {\n 1643→ const badgeRole = document.createElement('span');\n 1644→ badgeRole.className = `badge role-${e.role}`;\n 1645→ badgeRole.textContent = e.role;\n 1646→ badges.appendChild(badgeRole);\n 1647→ }\n 1648→\n 1649→ const summary = document.createElement('div');\n 1650→ summary.className = 'summary';\n 1651→ summary.textContent = e.summary;\n 1652→\n 1653→ body.appendChild(badges);\n 1654→ body.appendChild(summary);\n 1655→\n 1656→ row.appendChild(time);\n 1657→ row.appendChild(body);\n 1658→\n 1659→ row.addEventListener('click', () => selectEvent(e.id, { focusTimeline: true }));\n 1660→ fragment.appendChild(row);\n 1661→\n 1662→ if (e.date instanceof Date && !Number.isNaN(e.date.getTime())) prevDate = e.date;\n 1663→ }\n 1664→\n 1665→ els.timeline.appendChild(fragment);\n 1666→\n 1667→ if (state.selectedId !== null) {\n 1668→ const stillThere = state.filtered.some(e => e.id === state.selectedId);\n 1669→ if (!stillThere) clearSelection();\n 1670→ else selectEvent(state.selectedId, { scroll: false });\n 1671→ } else {\n 1672→ clearSelection({ syncHash: false });\n 1673→ }\n 1674→ }\n 1675→\n 1676→ function selectEvent(id, { scroll = false, focusTimeline = false } = {}) {\n 1677→ const e = state.filtered.find(item => item.id === id) || state.events.find(item => item.id === id);\n 1678→ if (!e) return;\n 1679→ state.selectedId = id;\n 1680→ state.selected = e;\n 1681→\n 1682→ if (focusTimeline) {\n 1683→ els.timeline.focus({ preventScroll: true });\n 1684→ }\n 1685→\n 1686→ for (const node of els.timeline.querySelectorAll('.event.selected')) {\n 1687→ node.classList.remove('selected');\n 1688→ }\n 1689→ const node = els.timeline.querySelector(`.event[data-event-id=\"${CSS.escape(String(id))}\"]`);\n 1690→ if (node) {\n 1691→ node.classList.add('selected');\n 1692→ updateDetailPlacement();\n 1693→ if (scroll) node.scrollIntoView({ block: 'center' });\n 1694→ }\n 1695→\n 1696→ els.detailJson.classList.remove('empty');\n 1697→ const titleParts = [\n 1698→ formatTimestamp(e.date, state.tzMode),\n 1699→ `#${e.line}`,\n 1700→ e.kind,\n 1701→ e.role ? `(${e.role})` : null,\n 1702→ ].filter(Boolean);\n 1703→ els.detailTitle.textContent = titleParts.join(' ');\n 1704→\n 1705→ const shouldTruncate = els.truncateStrings.checked;\n 1706→ renderDetailImages(e.obj);\n 1707→ renderPrettyView(e.obj);\n 1708→ els.detailJson.textContent = stringifyJson(e.obj, shouldTruncate) || '';\n 1709→ els.copyJson.disabled = false;\n 1710→ els.copyRaw.disabled = false;\n 1711→ updateDetailPlacement();\n 1712→ syncUrlHashFromControls();\n 1713→ }\n 1714→\n 1715→ function extractImageUrls(obj) {\n 1716→ const urls = [];\n 1717→ const message = obj?.message;\n 1718→ if (!message || typeof message !== 'object') return urls;\n 1719→ const content = message.content;\n 1720→ if (!Array.isArray(content)) return urls;\n 1721→\n 1722→ for (const item of content) {\n 1723→ if (!item || typeof item !== 'object') continue;\n 1724→\n 1725→ // Direct image in content\n 1726→ if (item.type === 'image' && item.source?.type === 'base64' && item.source?.data) {\n 1727→ const mediaType = item.source.media_type || 'image/png';\n 1728→ urls.push(`data:${mediaType};base64,${item.source.data}`);\n 1729→ }\n 1730→\n 1731→ // Image in tool_result content\n 1732→ if (item.type === 'tool_result' && Array.isArray(item.content)) {\n 1733→ for (const rc of item.content) {\n 1734→ if (rc?.type === 'image' && rc.source?.type === 'base64' && rc.source?.data) {\n 1735→ const mediaType = rc.source.media_type || 'image/png';\n 1736→ urls.push(`data:${mediaType};base64,${rc.source.data}`);\n 1737→ }\n 1738→ }\n 1739→ }\n 1740→ }\n 1741→\n 1742→ return urls;\n 1743→ }\n 1744→\n 1745→ function escapeHtml(text) {\n 1746→ const div = document.createElement('div');\n 1747→ div.textContent = text;\n 1748→ return div.innerHTML;\n 1749→ }\n 1750→\n 1751→ // Initialize markdown-it with GFM-like settings\n 1752→ const md = window.markdownit({\n 1753→ html: false, // Disable raw HTML input\n 1754→ linkify: true, // Auto-convert URLs to links\n 1755→ typographer: true, // Smart quotes, dashes\n 1756→ breaks: true // Convert \\n to <br>\n 1757→ });\n 1758→\n 1759→ function renderMarkdown(text) {\n 1760→ if (typeof text !== 'string') return '';\n 1761→\n 1762→ // Render markdown to HTML\n 1763→ const html = md.render(text);\n 1764→\n 1765→ // Sanitize with DOMPurify to prevent XSS\n 1766→ return DOMPurify.sanitize(html);\n 1767→ }\n 1768→\n 1769→ function renderPrettyView(obj) {\n 1770→ els.detailPretty.innerHTML = '';\n 1771→\n 1772→ if (!obj || typeof obj !== 'object') {\n 1773→ els.detailPretty.hidden = true;\n 1774→ return;\n 1775→ }\n 1776→\n 1777→ const message = obj.message;\n 1778→ if (!message || typeof message !== 'object') {\n 1779→ els.detailPretty.hidden = true;\n 1780→ return;\n 1781→ }\n 1782→\n 1783→ const content = message.content;\n 1784→ const fragment = document.createDocumentFragment();\n 1785→ let hasContent = false;\n 1786→\n 1787→ // Handle string content (simple text message)\n 1788→ if (typeof content === 'string') {\n 1789→ const section = document.createElement('div');\n 1790→ section.className = 'pretty-section';\n 1791→ const label = document.createElement('div');\n 1792→ label.className = 'pretty-label';\n 1793→ label.textContent = message.role || 'Message';\n 1794→ const body = document.createElement('div');\n 1795→ body.className = 'pretty-content';\n 1796→ body.innerHTML = renderMarkdown(content);\n 1797→ section.appendChild(label);\n 1798→ section.appendChild(body);\n 1799→ fragment.appendChild(section);\n 1800→ hasContent = true;\n 1801→ }\n 1802→\n 1803→ // Handle array content\n 1804→ if (Array.isArray(content)) {\n 1805→ for (const item of content) {\n 1806→ if (!item || typeof item !== 'object') continue;\n 1807→\n 1808→ // Text content\n 1809→ if (item.type === 'text' && item.text) {\n 1810→ const section = document.createElement('div');\n 1811→ section.className = 'pretty-section';\n 1812→ const label = document.createElement('div');\n 1813→ label.className = 'pretty-label';\n 1814→ label.textContent = message.role === 'assistant' ? 'Response' : 'Message';\n 1815→ const body = document.createElement('div');\n 1816→ body.className = 'pretty-content';\n 1817→ body.innerHTML = renderMarkdown(item.text);\n 1818→ section.appendChild(label);\n 1819→ section.appendChild(body);\n 1820→ fragment.appendChild(section);\n 1821→ hasContent = true;\n 1822→ }\n 1823→\n 1824→ // Thinking content\n 1825→ if (item.type === 'thinking' && item.thinking) {\n 1826→ const section = document.createElement('div');\n 1827→ section.className = 'pretty-section';\n 1828→ const label = document.createElement('div');\n 1829→ label.className = 'pretty-label';\n 1830→ label.textContent = 'Thinking';\n 1831→ const body = document.createElement('div');\n 1832→ body.className = 'thinking-block pretty-content';\n 1833→ body.innerHTML = renderMarkdown(item.thinking);\n 1834→ section.appendChild(label);\n 1835→ section.appendChild(body);\n 1836→ fragment.appendChild(section);\n 1837→ hasContent = true;\n 1838→ }\n 1839→\n 1840→ // Tool use\n 1841→ if (item.type === 'tool_use') {\n 1842→ const section = document.createElement('div');\n 1843→ section.className = 'pretty-section';\n 1844→ const label = document.createElement('div');\n 1845→ label.className = 'pretty-label';\n 1846→ label.textContent = 'Tool Call';\n 1847→\n 1848→ const toolName = item.name || 'Unknown Tool';\n 1849→ const input = item.input || {};\n 1850→\n 1851→ // Special handling for Bash command\n 1852→ if (toolName === 'Bash' && input.command) {\n 1853→ const block = document.createElement('div');\n 1854→ block.className = 'command-block';\n 1855→\n 1856→ const header = document.createElement('div');\n 1857→ header.className = 'command-header';\n 1858→ const nameSpan = document.createElement('span');\n 1859→ nameSpan.className = 'tool-name';\n 1860→ nameSpan.textContent = toolName;\n 1861→ header.appendChild(nameSpan);\n 1862→ if (input.description) {\n 1863→ const descSpan = document.createElement('span');\n 1864→ descSpan.className = 'tool-desc';\n 1865→ descSpan.textContent = input.description;\n 1866→ header.appendChild(descSpan);\n 1867→ }\n 1868→\n 1869→ const cmdInput = document.createElement('div');\n 1870→ cmdInput.className = 'command-input';\n 1871→ cmdInput.textContent = input.command;\n 1872→\n 1873→ block.appendChild(header);\n 1874→ block.appendChild(cmdInput);\n 1875→ section.appendChild(label);\n 1876→ section.appendChild(block);\n 1877→ }\n 1878→ // Special handling for file operations\n 1879→ else if (['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(toolName)) {\n 1880→ const block = document.createElement('div');\n 1881→ block.className = 'command-block';\n 1882→\n 1883→ const header = document.createElement('div');\n 1884→ header.className = 'command-header';\n 1885→ const nameSpan = document.createElement('span');\n 1886→ nameSpan.className = 'tool-name';\n 1887→ nameSpan.textContent = toolName;\n 1888→ header.appendChild(nameSpan);\n 1889→\n 1890→ block.appendChild(header);\n 1891→\n 1892→ const params = document.createElement('div');\n 1893→ params.className = 'tool-params';\n 1894→ params.style.padding = '10px 12px';\n 1895→\n 1896→ for (const [key, value] of Object.entries(input)) {\n 1897→ const param = document.createElement('div');\n 1898→ param.className = 'tool-param';\n 1899→ const paramName = document.createElement('span');\n 1900→ paramName.className = 'tool-param-name';\n 1901→ paramName.textContent = key;\n 1902→ const paramValue = document.createElement('span');\n 1903→ paramValue.className = 'tool-param-value';\n 1904→ if (typeof value === 'string') {\n 1905→ paramValue.textContent = value.length > 500 ? value.slice(0, 500) + '...' : value;\n 1906→ } else {\n 1907→ paramValue.textContent = JSON.stringify(value, null, 2);\n 1908→ }\n 1909→ param.appendChild(paramName);\n 1910→ param.appendChild(paramValue);\n 1911→ params.appendChild(param);\n 1912→ }\n 1913→\n 1914→ block.appendChild(params);\n 1915→ section.appendChild(label);\n 1916→ section.appendChild(block);\n 1917→ }\n 1918→ // Generic tool display\n 1919→ else {\n 1920→ const block = document.createElement('div');\n 1921→ block.className = 'command-block';\n 1922→\n 1923→ const header = document.createElement('div');\n 1924→ header.className = 'command-header';\n 1925→ const nameSpan = document.createElement('span');\n 1926→ nameSpan.className = 'tool-name';\n 1927→ nameSpan.textContent = toolName;\n 1928→ header.appendChild(nameSpan);\n 1929→\n 1930→ block.appendChild(header);\n 1931→\n 1932→ if (Object.keys(input).length > 0) {\n 1933→ const params = document.createElement('div');\n 1934→ params.className = 'tool-params';\n 1935→ params.style.padding = '10px 12px';\n 1936→\n 1937→ for (const [key, value] of Object.entries(input)) {\n 1938→ const param = document.createElement('div');\n 1939→ param.className = 'tool-param';\n 1940→ const paramName = document.createElement('span');\n 1941→ paramName.className = 'tool-param-name';\n 1942→ paramName.textContent = key;\n 1943→ const paramValue = document.createElement('span');\n 1944→ paramValue.className = 'tool-param-value';\n 1945→ if (typeof value === 'string') {\n 1946→ paramValue.textContent = value.length > 1000 ? value.slice(0, 1000) + '...' : value;\n 1947→ } else {\n 1948→ paramValue.textContent = JSON.stringify(value, null, 2);\n 1949→ }\n 1950→ param.appendChild(paramName);\n 1951→ param.appendChild(paramValue);\n 1952→ params.appendChild(param);\n 1953→ }\n 1954→\n 1955→ block.appendChild(params);\n 1956→ }\n 1957→\n 1958→ section.appendChild(label);\n 1959→ section.appendChild(block);\n 1960→ }\n 1961→\n 1962→ fragment.appendChild(section);\n 1963→ hasContent = true;\n 1964→ }\n 1965→\n 1966→ // Tool result\n 1967→ if (item.type === 'tool_result') {\n 1968→ const section = document.createElement('div');\n 1969→ section.className = 'pretty-section';\n 1970→ const label = document.createElement('div');\n 1971→ label.className = 'pretty-label';\n 1972→ label.textContent = item.is_error ? 'Tool Error' : 'Tool Result';\n 1973→\n 1974→ const resultContent = item.content;\n 1975→ let resultText = '';\n 1976→\n 1977→ if (typeof resultContent === 'string') {\n 1978→ resultText = resultContent;\n 1979→ } else if (Array.isArray(resultContent)) {\n 1980→ for (const rc of resultContent) {\n 1981→ if (rc?.type === 'text' && rc.text) {\n 1982→ resultText += rc.text;\n 1983→ }\n 1984→ }\n 1985→ }\n 1986→\n 1987→ if (resultText) {\n 1988→ const block = document.createElement('div');\n 1989→ block.className = 'pretty-content' + (item.is_error ? ' result-error' : '');\n 1990→ const displayText = resultText.length > 5000 ? resultText.slice(0, 5000) + '\\n... [truncated]' : resultText;\n 1991→ block.innerHTML = renderMarkdown(displayText);\n 1992→ section.appendChild(label);\n 1993→ section.appendChild(block);\n 1994→ fragment.appendChild(section);\n 1995→ hasContent = true;\n 1996→ }\n 1997→ }\n 1998→ }\n 1999→ }\n 2000→\n 2001→ if (hasContent) {\n 2002→ els.detailPretty.appendChild(fragment);\n 2003→ els.detailPretty.hidden = false;\n 2004→ } else {\n 2005→ els.detailPretty.hidden = true;\n 2006→ }\n 2007→ }\n 2008→\n 2009→ function openImageModal(src, altText = '') {\n 2010→ if (!src) return;\n 2011→ lastActiveElementBeforeModal = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n 2012→ lastBodyOverflow = document.body.style.overflow;\n 2013→ document.body.style.overflow = 'hidden';\n 2014→\n 2015→ els.imageModalImg.src = src;\n 2016→ els.imageModalImg.alt = altText;\n 2017→ els.imageModal.classList.add('open');\n 2018→ els.imageModal.setAttribute('aria-hidden', 'false');\n 2019→ els.imageModalClose.focus();\n 2020→ }\n 2021→\n 2022→ function closeImageModal() {\n 2023→ if (!els.imageModal.classList.contains('open')) return;\n 2024→ els.imageModal.classList.remove('open');\n 2025→ els.imageModal.setAttribute('aria-hidden', 'true');\n 2026→ els.imageModalImg.removeAttribute('src');\n 2027→ els.imageModalImg.alt = '';\n 2028→ document.body.style.overflow = lastBodyOverflow ?? '';\n 2029→\n 2030→ if (lastActiveElementBeforeModal) {\n 2031→ lastActiveElementBeforeModal.focus();\n 2032→ }\n 2033→ lastActiveElementBeforeModal = null;\n 2034→ lastBodyOverflow = null;\n 2035→ }\n 2036→\n 2037→ function renderDetailImages(obj) {\n 2038→ const urls = extractImageUrls(obj);\n 2039→ els.detailImages.innerHTML = '';\n 2040→ if (!urls.length) {\n 2041→ els.detailImages.hidden = true;\n 2042→ return;\n 2043→ }\n 2044→ const fragment = document.createDocumentFragment();\n 2045→ urls.forEach((src, idx) => {\n 2046→ const btn = document.createElement('button');\n 2047→ btn.type = 'button';\n 2048→ btn.className = 'thumb';\n 2049→ btn.title = `Open image ${idx + 1}`;\n 2050→ const img = document.createElement('img');\n 2051→ img.loading = 'lazy';\n 2052→ img.src = src;\n 2053→ img.alt = `Image ${idx + 1}`;\n 2054→ btn.appendChild(img);\n 2055→ btn.addEventListener('click', () => openImageModal(src, `Image ${idx + 1}`));\n 2056→ fragment.appendChild(btn);\n 2057→ });\n 2058→ els.detailImages.appendChild(fragment);\n 2059→ els.detailImages.hidden = false;\n 2060→ }\n 2061→\n 2062→ function isEditableElement(el) {\n 2063→ if (!(el instanceof HTMLElement)) return false;\n 2064→ if (el.isContentEditable) return true;\n 2065→ const tag = el.tagName;\n 2066→ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';\n 2067→ }\n 2068→\n 2069→ function moveSelection(delta) {\n 2070→ if (!state.filtered.length) return;\n 2071→ const idx = state.filtered.findIndex(e => e.id === state.selectedId);\n 2072→ if (idx === -1) {\n 2073→ selectEvent(state.filtered[0].id, { scroll: true });\n 2074→ return;\n 2075→ }\n 2076→ const nextIdx = Math.max(0, Math.min(state.filtered.length - 1, idx + delta));\n 2077→ if (nextIdx === idx) return;\n 2078→ selectEvent(state.filtered[nextIdx].id, { scroll: true });\n 2079→ }\n 2080→\n 2081→ async function copyText(text) {\n 2082→ if (typeof text !== 'string') return;\n 2083→ try {\n 2084→ await navigator.clipboard.writeText(text);\n 2085→ setStatus('Copied to clipboard.', 'good');\n 2086→ } catch (error) {\n 2087→ const ta = document.createElement('textarea');\n 2088→ ta.value = text;\n 2089→ ta.style.position = 'fixed';\n 2090→ ta.style.left = '-9999px';\n 2091→ document.body.appendChild(ta);\n 2092→ ta.select();\n 2093→ document.execCommand('copy');\n 2094→ document.body.removeChild(ta);\n 2095→ setStatus('Copied to clipboard.', 'good');\n 2096→ }\n 2097→ }\n 2098→\n 2099→ function extractUserPrompts() {\n 2100→ const prompts = [];\n 2101→ for (const e of state.events) {\n 2102→ // Filter: type=user, content-type includes text, role=user\n 2103→ if (e.kind !== 'user') continue;\n 2104→ if (e.role !== 'user') continue;\n 2105→ if (!e.contentTypes.includes('text')) continue;\n 2106→\n 2107→ const message = e.obj?.message;\n 2108→ if (!message) continue;\n 2109→\n 2110→ const content = message.content;\n 2111→ if (typeof content === 'string') {\n 2112→ prompts.push(content);\n 2113→ } else if (Array.isArray(content)) {\n 2114→ for (const item of content) {\n 2115→ if (item?.type === 'text' && item.text) {\n 2116→ prompts.push(item.text);\n 2117→ }\n 2118→ }\n 2119→ }\n 2120→ }\n 2121→ return prompts.join('\\n\\n');\n 2122→ }\n 2123→\n 2124→ function openPromptsModal(text) {\n 2125→ lastActiveElementBeforePrompts = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n 2126→ lastBodyOverflowBeforePrompts = document.body.style.overflow;\n 2127→ document.body.style.overflow = 'hidden';\n 2128→\n 2129→ els.promptsText.value = text || '';\n 2130→ els.promptsModal.classList.add('open');\n 2131→ els.promptsModal.setAttribute('aria-hidden', 'false');\n 2132→ els.promptsClose.focus();\n 2133→ els.promptsText.scrollTop = 0;\n 2134→ }\n 2135→\n 2136→ function closePromptsModal() {\n 2137→ if (!els.promptsModal.classList.contains('open')) return;\n 2138→ els.promptsModal.classList.remove('open');\n 2139→ els.promptsModal.setAttribute('aria-hidden', 'true');\n 2140→ els.promptsText.value = '';\n 2141→ document.body.style.overflow = lastBodyOverflowBeforePrompts ?? '';\n 2142→\n 2143→ if (lastActiveElementBeforePrompts) lastActiveElementBeforePrompts.focus();\n 2144→ lastActiveElementBeforePrompts = null;\n 2145→ lastBodyOverflowBeforePrompts = null;\n 2146→ }\n 2147→\n 2148→ function setTimezoneMode(mode) {\n 2149→ state.tzMode = mode;\n 2150→ els.tzLocal.classList.toggle('active', mode === 'local');\n 2151→ els.tzUtc.classList.toggle('active', mode === 'utc');\n 2152→ applyFilters();\n 2153→ if (state.selectedId !== null) selectEvent(state.selectedId);\n 2154→ syncUrlHashFromControls();\n 2155→ }\n 2156→\n 2157→ els.promptsClose.addEventListener('click', closePromptsModal);\n 2158→ els.promptsModal.addEventListener('click', (event) => {\n 2159→ if (event.target === els.promptsModal) closePromptsModal();\n 2160→ });\n 2161→ els.promptsCopy.addEventListener('click', async () => {\n 2162→ const text = els.promptsText.value || '';\n 2163→ if (!text.trim()) {\n 2164→ setStatus('No user prompts found.', 'warn');\n 2165→ return;\n 2166→ }\n 2167→ await copyText(text);\n 2168→ if (promptsCopiedTimer) clearTimeout(promptsCopiedTimer);\n 2169→ const originalText = els.promptsCopy.textContent;\n 2170→ els.promptsCopy.textContent = 'Copied!';\n 2171→ promptsCopiedTimer = setTimeout(() => {\n 2172→ els.promptsCopy.textContent = originalText;\n 2173→ promptsCopiedTimer = null;\n 2174→ }, 1500);\n 2175→ });\n 2176→ els.showPrompts.addEventListener('click', () => {\n 2177→ const text = extractUserPrompts();\n 2178→ if (!text) {\n 2179→ setStatus('No user prompts found.', 'warn');\n 2180→ return;\n 2181→ }\n 2182→ openPromptsModal(text);\n 2183→ });\n 2184→\n 2185→ els.imageModalClose.addEventListener('click', closeImageModal);\n 2186→ els.imageModal.addEventListener('click', (event) => {\n 2187→ if (event.target === els.imageModal) closeImageModal();\n 2188→ });\n 2189→ document.addEventListener('keydown', (event) => {\n 2190→ if (event.key !== 'Escape') return;\n 2191→ if (els.promptsModal.classList.contains('open')) closePromptsModal();\n 2192→ if (els.imageModal.classList.contains('open')) closeImageModal();\n 2193→ });\n 2194→\n 2195→ document.addEventListener('keydown', (event) => {\n 2196→ if (els.imageModal.classList.contains('open')) return;\n 2197→ if (els.promptsModal.classList.contains('open')) return;\n 2198→ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n 2199→ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;\n 2200→ if (isEditableElement(document.activeElement)) return;\n 2201→ if (document.activeElement !== els.timeline && !els.timeline.contains(document.activeElement)) return;\n 2202→ if (state.selectedId === null) return;\n 2203→ event.preventDefault();\n 2204→ moveSelection(event.key === 'ArrowUp' ? -1 : 1);\n 2205→ });\n 2206→\n 2207→ if (typeof wideLayoutMq.addEventListener === 'function') {\n 2208→ wideLayoutMq.addEventListener('change', updateDetailPlacement);\n 2209→ } else if (typeof wideLayoutMq.addListener === 'function') {\n 2210→ wideLayoutMq.addListener(updateDetailPlacement);\n 2211→ }\n 2212→\n 2213→ function setTab(tabName) {\n 2214→ for (const tab of document.querySelectorAll('.tab')) {\n 2215→ const active = tab.dataset.tab === tabName;\n 2216→ tab.classList.toggle('active', active);\n 2217→ tab.setAttribute('aria-selected', active ? 'true' : 'false');\n 2218→ }\n 2219→ for (const panel of document.querySelectorAll('[data-panel]')) {\n 2220→ panel.hidden = panel.dataset.panel !== tabName;\n 2221→ }\n 2222→ }\n 2223→\n 2224→ function setShareUrlParam(url) {\n 2225→ const current = new URL(window.location.href);\n 2226→ if (url) current.searchParams.set('url', url);\n 2227→ else current.searchParams.delete('url');\n 2228→ window.history.replaceState(null, '', current);\n 2229→ }\n 2230→\n 2231→ async function fetchAndRenderUrl(url, { syncUrl = true } = {}) {\n 2232→ const trimmed = (url || '').trim();\n 2233→ if (!trimmed) {\n 2234→ setStatus('Enter a URL first.', 'error');\n 2235→ return;\n 2236→ }\n 2237→ if (syncUrl) setShareUrlParam(trimmed);\n 2238→ setStatus(`Fetching ${trimmed}...`, 'warn');\n 2239→ try {\n 2240→ const resp = await fetch(trimmed, { mode: 'cors' });\n 2241→ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n 2242→ const text = await resp.text();\n 2243→ await loadText(text, trimmed);\n 2244→ } catch (error) {\n 2245→ setStatus(`Failed to fetch URL: ${String(error)}`, 'error');\n 2246→ }\n 2247→ }\n 2248→\n 2249→ async function loadText(text, sourceLabel) {\n 2250→ const started = performance.now();\n 2251→ state.sourceLabel = sourceLabel || null;\n 2252→ setStatus(`Parsing ${sourceLabel || 'content'}...`, 'warn');\n 2253→ closePromptsModal();\n 2254→ els.showPrompts.disabled = true;\n 2255→ clearSelection({ syncHash: false });\n 2256→\n 2257→ const lines = text.split(/\\r?\\n/);\n 2258→ const events = [];\n 2259→ const errors = [];\n 2260→ let idCounter = 0;\n 2261→\n 2262→ for (let i = 0; i < lines.length; i++) {\n 2263→ const line = lines[i];\n 2264→ if (!line.trim()) continue;\n 2265→ const parsed = safeJsonParse(line);\n 2266→ if (!parsed.ok) {\n 2267→ errors.push({ line: i + 1, error: parsed.error });\n 2268→ continue;\n 2269→ }\n 2270→ const obj = parsed.value;\n 2271→ const ts = typeof obj.timestamp === 'string' ? new Date(obj.timestamp) : null;\n 2272→ const kind = obj.type || '(unknown)';\n 2273→ const message = obj.message && typeof obj.message === 'object' ? obj.message : null;\n 2274→ const contentTypes = message ? getContentTypes(message) : [];\n 2275→ const role = message?.role ?? null;\n 2276→\n 2277→ const summary = summarizeEvent(obj);\n 2278→ events.push({\n 2279→ id: ++idCounter,\n 2280→ line: i + 1,\n 2281→ raw: line,\n 2282→ obj,\n 2283→ date: ts,\n 2284→ kind,\n 2285→ contentTypes,\n 2286→ role,\n 2287→ summary,\n 2288→ search: buildSearchText(obj, summary),\n 2289→ });\n 2290→ }\n 2291→\n 2292→ state.events = events;\n 2293→ els.showPrompts.disabled = !events.length;\n 2294→ updateFilterOptions(events);\n 2295→ applyFilters();\n 2296→ applySelectionFromHash();\n 2297→ syncUrlHashFromControls();\n 2298→\n 2299→ const durationMs = Math.round(performance.now() - started);\n 2300→ if (errors.length) {\n 2301→ const first = errors[0];\n 2302→ setStatus(\n 2303→ `Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.\\n` +\n 2304→ `Could not parse ${errors.length} line(s). First error at line ${first.line}: ${String(first.error)}`,\n 2305→ 'warn'\n 2306→ );\n 2307→ } else {\n 2308→ setStatus(`Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.`, 'good');\n 2309→ }\n 2310→ }\n 2311→\n 2312→ async function handleFile(file) {\n 2313→ if (!file) return;\n 2314→ setShareUrlParam(null);\n 2315→ try {\n 2316→ const text = await file.text();\n 2317→ await loadText(text, file.name);\n 2318→ } catch (error) {\n 2319→ setStatus(`Failed to read file: ${String(error)}`, 'error');\n 2320→ }\n 2321→ }\n 2322→\n 2323→ els.dropzone.addEventListener('click', () => els.fileInput.click());\n 2324→ els.dropzone.addEventListener('keydown', (e) => {\n 2325→ if (e.key === 'Enter' || e.key === ' ') {\n 2326→ e.preventDefault();\n 2327→ els.fileInput.click();\n 2328→ }\n 2329→ });\n 2330→ els.fileInput.addEventListener('change', () => handleFile(els.fileInput.files?.[0]));\n 2331→\n 2332→ els.dropzone.addEventListener('dragover', (e) => {\n 2333→ e.preventDefault();\n 2334→ els.dropzone.classList.add('dragover');\n 2335→ });\n 2336→ els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('dragover'));\n 2337→ els.dropzone.addEventListener('drop', (e) => {\n 2338→ e.preventDefault();\n 2339→ els.dropzone.classList.remove('dragover');\n 2340→ const file = e.dataTransfer?.files?.[0];\n 2341→ if (file) handleFile(file);\n 2342→ });\n 2343→\n 2344→ els.renderPaste.addEventListener('click', async () => {\n 2345→ const text = els.pasteInput.value || '';\n 2346→ if (!text.trim()) {\n 2347→ setStatus('Paste some JSONL content first.', 'error');\n 2348→ return;\n 2349→ }\n 2350→ setShareUrlParam(null);\n 2351→ await loadText(text, 'pasted content');\n 2352→ });\n 2353→\n 2354→ els.fetchUrl.addEventListener('click', async () => {\n 2355→ await fetchAndRenderUrl(els.urlInput.value, { syncUrl: true });\n 2356→ });\n 2357→\n 2358→ for (const tab of document.querySelectorAll('.tab')) {\n 2359→ tab.addEventListener('click', () => setTab(tab.dataset.tab));\n 2360→ }\n 2361→\n 2362→ els.tzLocal.addEventListener('click', () => setTimezoneMode('local'));\n 2363→ els.tzUtc.addEventListener('click', () => setTimezoneMode('utc'));\n 2364→\n 2365→ els.search.addEventListener('input', () => {\n 2366→ state.filters.query = els.search.value.trim().toLowerCase();\n 2367→ applyFilters();\n 2368→ syncUrlHashFromControls();\n 2369→ });\n 2370→ els.typeFilter.addEventListener('change', () => {\n 2371→ state.filters.type = els.typeFilter.value;\n 2372→ applyFilters();\n 2373→ syncUrlHashFromControls();\n 2374→ });\n 2375→ els.contentTypeFilter.addEventListener('change', () => {\n 2376→ state.filters.contentType = els.contentTypeFilter.value;\n 2377→ applyFilters();\n 2378→ syncUrlHashFromControls();\n 2379→ });\n 2380→ els.roleFilter.addEventListener('change', () => {\n 2381→ state.filters.role = els.roleFilter.value;\n 2382→ applyFilters();\n 2383→ syncUrlHashFromControls();\n 2384→ });\n 2385→ els.hideThinking.addEventListener('change', () => {\n 2386→ state.filters.hideThinking = els.hideThinking.checked;\n 2387→ applyFilters();\n 2388→ syncUrlHashFromControls();\n 2389→ });\n 2390→ els.truncateStrings.addEventListener('change', () => {\n 2391→ if (state.selectedId !== null) selectEvent(state.selectedId);\n 2392→ syncUrlHashFromControls();\n 2393→ });\n 2394→\n 2395→ els.clearFilters.addEventListener('click', () => {\n 2396→ els.search.value = '';\n 2397→ state.filters.query = '';\n 2398→ state.filters.type = 'all';\n 2399→ state.filters.contentType = 'all';\n 2400→ state.filters.role = 'all';\n 2401→ state.filters.hideThinking = false;\n 2402→ els.hideThinking.checked = false;\n 2403→ updateFilterOptions(state.events);\n 2404→ applyFilters();\n 2405→ syncUrlHashFromControls();\n 2406→ });\n 2407→\n 2408→ els.copyJson.addEventListener('click', () => {\n 2409→ if (!state.selected) return;\n 2410→ copyText(stringifyJson(state.selected.obj, false));\n 2411→ });\n 2412→ els.copyRaw.addEventListener('click', () => {\n 2413→ if (!state.selected) return;\n 2414→ copyText(state.selected.raw);\n 2415→ });\n 2416→\n 2417→ async function initFromQuery() {\n 2418→ const url = new URLSearchParams(window.location.search).get('url');\n 2419→ if (!url) return;\n 2420→ setTab('url');\n 2421→ els.urlInput.value = url;\n 2422→ await fetchAndRenderUrl(url, { syncUrl: false });\n 2423→ }\n 2424→\n 2425→ // Initialize select options for the empty state.\n 2426→ applyControlsFromHash();\n 2427→ updateFilterOptions([]);\n 2428→ syncUrlHashFromControls();\n 2429→ window.addEventListener('hashchange', () => {\n 2430→ applyControlsFromHash();\n 2431→ updateFilterOptions(state.events);\n 2432→ applyFilters();\n 2433→ applySelectionFromHash();\n 2434→ });\n 2435→ void initFromQuery();\n 2436→ </script>\n 2437→</body>\n 2438→</html>\n 2439→\n\n<system-reminder>\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n</system-reminder>\n"}]},"uuid":"b5525cfd-15ae-4823-8d33-bbdb905d5bc5","timestamp":"2025-12-19T04:26:10.024Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Claude Code Timeline Viewer</title>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.3/purify.min.js\" integrity=\"sha384-osZDKVu4ipZP703HmPOhWdyBajcFyjX2Psjk//TG1Rc0AdwEtuToaylrmcK3LdAl\" crossorigin=\"anonymous\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/markdown-it/4.4.0/markdown-it.min.js\" integrity=\"sha384-cxGp+QDeMxACP73aA0Lj0Sc/h4z1X73+umcvCyI0Vbyh3OYxgrPI+0mxG9slsMTN\" crossorigin=\"anonymous\"></script>\n <style>\n :root {\n color-scheme: light dark;\n --bg: #0b0c0f;\n --panel: #12141a;\n --panel-2: #0f1116;\n --text: #e8eaf0;\n --muted: #a6adbf;\n --line: rgba(255, 255, 255, 0.10);\n --line-2: rgba(255, 255, 255, 0.16);\n --accent: #6aa9ff;\n --accent-2: #8a5cff;\n --good: #46d39a;\n --warn: #ffcc66;\n --bad: #ff6b6b;\n --shadow: 0 18px 50px rgba(0, 0, 0, 0.35);\n --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,\n \"Apple Color Emoji\", \"Segoe UI Emoji\";\n }\n\n @media (prefers-color-scheme: light) {\n :root {\n --bg: #f7f8fc;\n --panel: #ffffff;\n --panel-2: #ffffff;\n --text: #10121a;\n --muted: #5b647a;\n --line: rgba(16, 18, 26, 0.12);\n --line-2: rgba(16, 18, 26, 0.18);\n --shadow: 0 18px 50px rgba(16, 18, 26, 0.10);\n }\n }\n\n * { box-sizing: border-box; }\n html, body { height: 100%; }\n body {\n margin: 0;\n font-family: var(--sans);\n background: radial-gradient(1200px 600px at 10% -10%, rgba(106, 169, 255, 0.18), transparent 45%),\n radial-gradient(1200px 600px at 90% -20%, rgba(138, 92, 255, 0.16), transparent 50%),\n var(--bg);\n color: var(--text);\n }\n\n .wrap {\n max-width: none;\n margin: 0;\n padding: 22px 16px 42px;\n }\n\n header {\n display: grid;\n gap: 10px;\n margin-bottom: 14px;\n }\n\n h1 {\n margin: 0;\n font-size: clamp(22px, 3vw, 32px);\n letter-spacing: 0.2px;\n }\n\n .sub {\n margin: 6px 0 0;\n color: var(--muted);\n line-height: 1.35;\n max-width: 70ch;\n }\n\n .controls {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n align-items: center;\n justify-content: flex-start;\n }\n\n .controls .field {\n display: grid;\n gap: 4px;\n min-width: 180px;\n }\n\n .label {\n font-size: 12px;\n color: var(--muted);\n }\n\n input[type=\"search\"], input[type=\"url\"], select, textarea {\n font: inherit;\n color: inherit;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n border-radius: 10px;\n padding: 10px 11px;\n outline: none;\n }\n\n input[type=\"search\"]:focus, input[type=\"url\"]:focus, select:focus, textarea:focus {\n border-color: color-mix(in srgb, var(--accent) 60%, var(--line));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .segmented {\n display: inline-flex;\n border: 1px solid var(--line);\n border-radius: 999px;\n overflow: hidden;\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n }\n\n .segmented button {\n border: none;\n padding: 9px 12px;\n font: inherit;\n background: transparent;\n color: var(--muted);\n cursor: pointer;\n min-width: 74px;\n }\n\n .segmented button.active {\n background: color-mix(in srgb, var(--accent) 16%, transparent);\n color: var(--text);\n }\n\n .segmented button:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);\n outline-offset: -2px;\n }\n\n .btn {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n color: var(--text);\n border-radius: 10px;\n padding: 10px 12px;\n font: inherit;\n cursor: pointer;\n transition: transform 80ms ease, border-color 120ms ease;\n }\n\n .btn:hover { border-color: var(--line-2); }\n .btn:active { transform: translateY(1px); }\n .btn.primary {\n border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n background: color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .btn:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n }\n\n .panel {\n background: var(--panel);\n border: 1px solid var(--line);\n border-radius: 16px;\n box-shadow: var(--shadow);\n }\n\n .input-panel {\n padding: 14px;\n margin-bottom: 14px;\n }\n\n .filters-panel {\n padding: 14px;\n margin-bottom: 14px;\n }\n\n .input-grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 14px;\n align-items: stretch;\n }\n\n @media (min-width: 920px) {\n .input-grid {\n grid-template-columns: 380px 1fr;\n }\n }\n\n .dropzone {\n border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--line));\n border-radius: 14px;\n background: linear-gradient(\n 135deg,\n color-mix(in srgb, var(--accent) 12%, transparent),\n transparent 45%,\n color-mix(in srgb, var(--accent-2) 10%, transparent)\n );\n padding: 16px;\n display: grid;\n gap: 6px;\n cursor: pointer;\n min-height: 124px;\n align-content: center;\n }\n\n .dropzone.dragover {\n border-style: solid;\n border-color: color-mix(in srgb, var(--accent) 70%, var(--line));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .dropzone strong { font-size: 15px; }\n .muted { color: var(--muted); }\n .tiny { font-size: 12px; color: var(--muted); }\n\n .tabs {\n display: flex;\n gap: 8px;\n margin-bottom: 10px;\n flex-wrap: wrap;\n align-items: center;\n }\n\n .tab {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n color: var(--muted);\n border-radius: 999px;\n padding: 7px 10px;\n cursor: pointer;\n font: inherit;\n }\n\n .tab.active {\n color: var(--text);\n border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n }\n\n textarea {\n width: 100%;\n min-height: 190px;\n resize: vertical;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n }\n\n .row {\n display: flex;\n gap: 10px;\n flex-wrap: wrap;\n align-items: center;\n margin-top: 10px;\n }\n\n .status {\n margin-top: 12px;\n padding: 10px 12px;\n border-radius: 12px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n display: none;\n white-space: pre-wrap;\n word-break: break-word;\n }\n .status.visible { display: block; }\n .status.error { border-color: color-mix(in srgb, var(--bad) 60%, var(--line)); }\n .status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n .status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n\n main {\n display: grid;\n gap: 14px;\n grid-template-columns: 1fr;\n min-height: 560px;\n }\n\n @media (min-width: 1060px) {\n main {\n grid-template-columns: 1fr 520px;\n align-items: stretch;\n }\n }\n\n .timeline-panel {\n display: grid;\n grid-template-rows: auto 1fr;\n min-height: 560px;\n }\n\n .stats {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n }\n\n .stats .big {\n font-size: 14px;\n font-weight: 600;\n }\n\n .timeline {\n overflow: auto;\n padding: 8px 0;\n }\n\n .timeline:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);\n outline-offset: -2px;\n border-radius: 14px;\n }\n\n .day-sep {\n position: sticky;\n top: 0;\n z-index: 2;\n padding: 8px 14px;\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n border-top: 1px solid var(--line);\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n color: var(--muted);\n backdrop-filter: blur(6px);\n }\n\n .event {\n display: grid;\n grid-template-columns: 176px 1fr;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid color-mix(in srgb, var(--line) 85%, transparent);\n cursor: pointer;\n }\n\n .event:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }\n .event.selected {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);\n outline-offset: -2px;\n }\n\n .time {\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.35;\n color: var(--muted);\n display: grid;\n gap: 3px;\n }\n\n .time .ts {\n color: var(--text);\n font-weight: 600;\n }\n\n .badges {\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n align-items: center;\n margin-bottom: 6px;\n }\n\n .badge {\n font-size: 11px;\n font-family: var(--mono);\n padding: 2px 7px;\n border-radius: 999px;\n border: 1px solid var(--line);\n color: var(--muted);\n background: color-mix(in srgb, var(--panel-2) 90%, transparent);\n }\n\n .badge.k-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n .badge.k-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n .badge.k-file-history-snapshot { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); color: color-mix(in srgb, var(--warn) 75%, var(--text)); }\n\n .badge.ct-text { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); color: color-mix(in srgb, var(--good) 75%, var(--text)); }\n .badge.ct-thinking { border-color: color-mix(in srgb, var(--accent-2) 55%, var(--line)); color: color-mix(in srgb, var(--accent-2) 75%, var(--text)); }\n .badge.ct-tool_use { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); color: color-mix(in srgb, var(--accent) 75%, var(--text)); }\n .badge.ct-tool_result { border-color: color-mix(in srgb, #8690a8 55%, var(--line)); color: color-mix(in srgb, #8690a8 70%, var(--text)); }\n .badge.ct-image { border-color: color-mix(in srgb, #ff9f43 55%, var(--line)); color: color-mix(in srgb, #ff9f43 75%, var(--text)); }\n\n .badge.role-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n .badge.role-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n\n .summary {\n font-size: 13px;\n line-height: 1.35;\n color: var(--text);\n word-break: break-word;\n }\n\n .summary code {\n font-family: var(--mono);\n font-size: 12px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n padding: 2px 5px;\n border-radius: 7px;\n }\n\n .detail {\n display: grid;\n grid-template-rows: auto auto auto auto 1fr;\n min-height: 560px;\n }\n\n @media (min-width: 1060px) {\n .detail {\n position: sticky;\n top: 16px;\n align-self: start;\n max-height: calc(100vh - 32px);\n }\n }\n\n .detail.inline {\n position: static;\n top: auto;\n align-self: auto;\n max-height: none;\n min-height: 0;\n margin: 8px 14px 12px;\n box-shadow: none;\n }\n\n .detail.inline pre {\n max-height: 52vh;\n }\n\n .detail-head {\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n display: grid;\n gap: 8px;\n }\n\n .detail-title {\n font-size: 14px;\n font-weight: 700;\n line-height: 1.25;\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .detail-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-start;\n }\n\n .check {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--muted);\n font-size: 12px;\n user-select: none;\n }\n\n pre {\n margin: 0;\n padding: 12px 14px 16px;\n overflow: auto;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.45;\n color: color-mix(in srgb, var(--text) 92%, transparent);\n background: transparent;\n white-space: pre-wrap;\n word-break: break-word;\n }\n\n .detail-images {\n display: flex;\n gap: 10px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n overflow-x: auto;\n overscroll-behavior-x: contain;\n }\n\n .thumb {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n border-radius: 12px;\n padding: 0;\n cursor: pointer;\n width: 78px;\n height: 58px;\n overflow: hidden;\n flex: 0 0 auto;\n display: grid;\n place-items: stretch;\n }\n\n .thumb:hover { border-color: var(--line-2); }\n\n .thumb img {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: cover;\n }\n\n .image-modal {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: rgba(0, 0, 0, 0.68);\n display: none;\n padding: 18px;\n place-items: center;\n }\n\n .image-modal.open { display: grid; }\n\n .image-modal .dialog {\n position: relative;\n border-radius: 16px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 98%, transparent);\n box-shadow: var(--shadow);\n padding: 14px;\n max-width: min(1200px, 96vw);\n max-height: 92vh;\n display: grid;\n place-items: center;\n }\n\n .image-modal img {\n display: block;\n max-width: min(1140px, 92vw);\n max-height: 84vh;\n width: auto;\n height: auto;\n object-fit: contain;\n border-radius: 10px;\n background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n }\n\n .image-modal .close {\n position: absolute;\n top: 10px;\n right: 10px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n color: var(--text);\n border-radius: 999px;\n padding: 8px 10px;\n font: inherit;\n cursor: pointer;\n }\n\n .image-modal .close:hover { border-color: var(--line-2); }\n\n .empty {\n padding: 14px;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.35;\n }\n\n /* Pretty view styles */\n .detail-pretty {\n padding: 14px;\n border-bottom: 1px solid var(--line);\n overflow: auto;\n max-height: 50vh;\n }\n\n .detail.inline .detail-pretty {\n max-height: 40vh;\n }\n\n .pretty-section {\n margin-bottom: 14px;\n }\n\n .pretty-section:last-child {\n margin-bottom: 0;\n }\n\n .pretty-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--muted);\n margin-bottom: 6px;\n }\n\n .pretty-content {\n font-size: 13px;\n line-height: 1.5;\n }\n\n .pretty-content p {\n margin: 0 0 10px;\n }\n\n .pretty-content p:last-child {\n margin-bottom: 0;\n }\n\n .pretty-content h1, .pretty-content h2, .pretty-content h3,\n .pretty-content h4, .pretty-content h5, .pretty-content h6 {\n margin: 14px 0 8px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .pretty-content h1:first-child, .pretty-content h2:first-child,\n .pretty-content h3:first-child {\n margin-top: 0;\n }\n\n .pretty-content h1 { font-size: 1.4em; }\n .pretty-content h2 { font-size: 1.25em; }\n .pretty-content h3 { font-size: 1.1em; }\n\n .pretty-content ul, .pretty-content ol {\n margin: 8px 0;\n padding-left: 1.5em;\n list-style-position: outside;\n }\n\n .pretty-content li {\n margin: 4px 0;\n }\n\n .thinking-block ul, .thinking-block ol {\n padding-left: 2em;\n }\n\n .pretty-content code {\n font-family: var(--mono);\n font-size: 0.9em;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n padding: 2px 5px;\n border-radius: 5px;\n }\n\n .pretty-content pre {\n margin: 10px 0;\n padding: 12px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n overflow-x: auto;\n }\n\n .pretty-content pre code {\n background: none;\n border: none;\n padding: 0;\n font-size: 12px;\n }\n\n .pretty-content blockquote {\n margin: 10px 0;\n padding: 8px 12px;\n border-left: 3px solid var(--accent);\n background: color-mix(in srgb, var(--accent) 8%, transparent);\n border-radius: 0 8px 8px 0;\n }\n\n .pretty-content a {\n color: var(--accent);\n text-decoration: none;\n }\n\n .pretty-content a:hover {\n text-decoration: underline;\n }\n\n .pretty-content strong {\n font-weight: 600;\n }\n\n .pretty-content em {\n font-style: italic;\n }\n\n .pretty-content table {\n border-collapse: collapse;\n margin: 10px 0;\n width: 100%;\n font-size: 13px;\n }\n\n .pretty-content th,\n .pretty-content td {\n border: 1px solid var(--line);\n padding: 8px 10px;\n text-align: left;\n }\n\n .pretty-content th {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n font-weight: 600;\n }\n\n .pretty-content tr:hover {\n background: color-mix(in srgb, var(--accent) 6%, transparent);\n }\n\n .command-block {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n overflow: hidden;\n margin: 8px 0;\n }\n\n .command-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n }\n\n .command-header .tool-name {\n font-weight: 600;\n color: var(--accent);\n }\n\n .command-header .tool-desc {\n color: var(--muted);\n }\n\n .command-input {\n padding: 10px 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n border-bottom: 1px solid var(--line);\n }\n\n .command-input::before {\n content: '$ ';\n color: var(--good);\n font-weight: 600;\n }\n\n .command-output {\n padding: 10px 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 300px;\n overflow: auto;\n color: var(--muted);\n }\n\n .file-path {\n font-family: var(--mono);\n font-size: 12px;\n padding: 6px 10px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 8px;\n display: inline-block;\n margin: 4px 0;\n }\n\n .tool-params {\n display: grid;\n gap: 8px;\n margin-top: 8px;\n }\n\n .tool-param {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 8px;\n align-items: start;\n font-size: 12px;\n }\n\n .tool-param-name {\n font-family: var(--mono);\n font-weight: 600;\n color: var(--accent);\n padding: 2px 6px;\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-radius: 4px;\n }\n\n .tool-param-value {\n font-family: var(--mono);\n white-space: pre-wrap;\n word-break: break-word;\n padding: 2px 0;\n }\n\n .thinking-block {\n background: color-mix(in srgb, var(--accent-2) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--accent-2) 30%, var(--line));\n border-radius: 10px;\n padding: 12px 16px;\n font-size: 13px;\n line-height: 1.5;\n }\n\n .result-block {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n padding: 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 400px;\n overflow: auto;\n }\n\n .result-error {\n border-color: color-mix(in srgb, var(--bad) 50%, var(--line));\n background: color-mix(in srgb, var(--bad) 8%, transparent);\n }\n\n .json-toggle {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n border-bottom: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 50%, transparent);\n }\n\n .json-toggle-label {\n font-size: 12px;\n color: var(--muted);\n font-weight: 500;\n }\n\n .prompts-modal {\n position: fixed;\n inset: 0;\n z-index: 80;\n background: rgba(0, 0, 0, 0.68);\n display: none;\n padding: 18px;\n place-items: center;\n }\n\n .prompts-modal.open { display: grid; }\n\n .prompts-modal .dialog {\n position: relative;\n border-radius: 16px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 98%, transparent);\n box-shadow: var(--shadow);\n padding: 14px;\n width: min(1100px, 96vw);\n height: min(86vh, 780px);\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: 10px;\n }\n\n .prompts-head {\n display: flex;\n gap: 10px;\n align-items: center;\n justify-content: space-between;\n }\n\n .prompts-title {\n font-weight: 700;\n letter-spacing: 0.2px;\n }\n\n .prompts-modal textarea {\n width: 100%;\n height: 100%;\n resize: none;\n border-radius: 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.45;\n padding: 12px;\n }\n\n .prompts-actions {\n display: flex;\n gap: 10px;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n }\n </style>\n</head>\n <body>\n <div class=\"wrap\">\n <header>\n <div>\n <h1>Claude Code Timeline Viewer</h1>\n <p class=\"sub\">Open a Claude Code session <code>.jsonl</code> file and explore it as an interactive timeline. Times are shown in your local timezone by default. Find sessions in <code>~/.claude/projects/</code>.</p>\n </div>\n </header>\n\n <section class=\"panel input-panel\" aria-label=\"Input\">\n <div class=\"input-grid\">\n <div>\n <div id=\"dropzone\" class=\"dropzone\" tabindex=\"0\">\n <strong>Drop a <code>.jsonl</code> file here</strong>\n <div class=\"muted\">or click to choose a file</div>\n <div class=\"tiny\">Supported: file picker, drag-and-drop, paste, or URL fetch (CORS-enabled).</div>\n <input id=\"file-input\" type=\"file\" accept=\".jsonl,application/json,text/plain\" hidden />\n </div>\n </div>\n <div>\n <div class=\"tabs\" role=\"tablist\" aria-label=\"Input tabs\">\n <button type=\"button\" class=\"tab active\" data-tab=\"paste\" aria-selected=\"true\">Paste</button>\n <button type=\"button\" class=\"tab\" data-tab=\"url\" aria-selected=\"false\">URL</button>\n </div>\n\n <div id=\"panel-paste\" data-panel=\"paste\">\n <textarea id=\"paste-input\" placeholder=\"Paste JSONL content here...\"></textarea>\n <div class=\"row\">\n <button type=\"button\" class=\"btn primary\" id=\"render-paste\">Render pasted content</button>\n <div class=\"tiny\">Tip: the timeline uses one JSON object per line.</div>\n </div>\n </div>\n\n <div id=\"panel-url\" data-panel=\"url\" hidden>\n <input id=\"url-input\" type=\"url\" placeholder=\"https://example.com/session.jsonl\" />\n <div class=\"row\">\n <button type=\"button\" class=\"btn primary\" id=\"fetch-url\">Fetch and render</button>\n <div class=\"tiny\">The URL must allow cross-origin requests (CORS).</div>\n </div>\n </div>\n </div>\n </div>\n\n <div id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></div>\n </section>\n\n <section class=\"panel filters-panel\" aria-label=\"Filters\">\n <div class=\"controls\" aria-label=\"Timeline controls\">\n <div class=\"field\" style=\"min-width: 170px\">\n <div class=\"label\">Timezone</div>\n <div class=\"segmented\" role=\"group\" aria-label=\"Timezone toggle\">\n <button type=\"button\" id=\"tz-local\" class=\"active\">Local</button>\n <button type=\"button\" id=\"tz-utc\">UTC</button>\n </div>\n </div>\n <div class=\"field\" style=\"min-width: 220px\">\n <div class=\"label\">Search</div>\n <input id=\"search\" type=\"search\" placeholder=\"message, tool name, cwd...\" autocomplete=\"off\" />\n </div>\n <div class=\"field\" style=\"min-width: 190px\">\n <div class=\"label\">Type</div>\n <select id=\"type-filter\"></select>\n </div>\n <div class=\"field\" style=\"min-width: 210px\">\n <div class=\"label\">Content Type</div>\n <select id=\"content-type-filter\"></select>\n </div>\n <div class=\"field\" style=\"min-width: 160px\">\n <div class=\"label\">Role</div>\n <select id=\"role-filter\"></select>\n </div>\n <label class=\"check\" title=\"Hide thinking blocks (can be verbose)\">\n <input type=\"checkbox\" id=\"hide-thinking\" />\n Hide thinking\n </label>\n <button type=\"button\" class=\"btn\" id=\"clear-filters\">Clear</button>\n </div>\n </section>\n\n <div class=\"controls\" style=\"margin-bottom: 14px;\">\n <button type=\"button\" class=\"btn\" id=\"show-prompts\" disabled>Show prompts</button>\n </div>\n\n <main>\n <section class=\"panel timeline-panel\" aria-label=\"Timeline\">\n <div class=\"stats\">\n <div class=\"big\"><span id=\"stats-showing\">0</span> <span class=\"muted\">showing</span> <span class=\"muted\">/</span> <span id=\"stats-total\">0</span> <span class=\"muted\">events</span></div>\n <div class=\"muted\" id=\"stats-range\">-</div>\n </div>\n <div id=\"timeline\" class=\"timeline\" role=\"list\" tabindex=\"0\" aria-label=\"Events list\"></div>\n </section>\n\n <aside class=\"panel detail\" aria-label=\"Details\">\n <div class=\"detail-head\">\n <div class=\"detail-title\" id=\"detail-title\">Select an event</div>\n <div class=\"detail-actions\">\n <button type=\"button\" class=\"btn\" id=\"copy-json\" disabled>Copy JSON</button>\n <button type=\"button\" class=\"btn\" id=\"copy-raw\" disabled>Copy raw line</button>\n <label class=\"check\" title=\"Truncate very long strings in the JSON preview\">\n <input type=\"checkbox\" id=\"truncate-strings\" checked />\n Truncate long strings\n </label>\n </div>\n </div>\n <div id=\"detail-images\" class=\"detail-images\" hidden aria-label=\"Event images\"></div>\n <div id=\"detail-pretty\" class=\"detail-pretty\" hidden aria-label=\"Formatted event view\"></div>\n <div class=\"json-toggle\">\n <span class=\"json-toggle-label\">Raw JSON</span>\n </div>\n <pre id=\"detail-json\" aria-label=\"Selected event JSON\" class=\"empty\">Load a file to get started.</pre>\n </aside>\n </main>\n </div>\n\n <div id=\"image-modal\" class=\"image-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\">\n <div class=\"dialog\">\n <button type=\"button\" class=\"close\" id=\"image-modal-close\" aria-label=\"Close image\">Close</button>\n <img id=\"image-modal-img\" alt=\"\" />\n </div>\n </div>\n\n <div id=\"prompts-modal\" class=\"prompts-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\" aria-label=\"User prompts\">\n <div class=\"dialog\">\n <div class=\"prompts-head\">\n <div class=\"prompts-title\">User prompts</div>\n <button type=\"button\" class=\"btn\" id=\"prompts-close\" aria-label=\"Close prompts\">Close</button>\n </div>\n <textarea id=\"prompts-text\" readonly spellcheck=\"false\"></textarea>\n <div class=\"prompts-actions\">\n <button type=\"button\" class=\"btn primary\" id=\"prompts-copy\">Copy</button>\n </div>\n </div>\n </div>\n\n <script>\n const els = {\n dropzone: document.getElementById('dropzone'),\n fileInput: document.getElementById('file-input'),\n pasteInput: document.getElementById('paste-input'),\n renderPaste: document.getElementById('render-paste'),\n urlInput: document.getElementById('url-input'),\n fetchUrl: document.getElementById('fetch-url'),\n status: document.getElementById('status'),\n timeline: document.getElementById('timeline'),\n detailPanel: document.querySelector('aside.detail'),\n detailTitle: document.getElementById('detail-title'),\n detailImages: document.getElementById('detail-images'),\n detailPretty: document.getElementById('detail-pretty'),\n detailJson: document.getElementById('detail-json'),\n copyJson: document.getElementById('copy-json'),\n copyRaw: document.getElementById('copy-raw'),\n truncateStrings: document.getElementById('truncate-strings'),\n tzLocal: document.getElementById('tz-local'),\n tzUtc: document.getElementById('tz-utc'),\n search: document.getElementById('search'),\n typeFilter: document.getElementById('type-filter'),\n contentTypeFilter: document.getElementById('content-type-filter'),\n roleFilter: document.getElementById('role-filter'),\n hideThinking: document.getElementById('hide-thinking'),\n statsShowing: document.getElementById('stats-showing'),\n statsTotal: document.getElementById('stats-total'),\n statsRange: document.getElementById('stats-range'),\n clearFilters: document.getElementById('clear-filters'),\n imageModal: document.getElementById('image-modal'),\n imageModalImg: document.getElementById('image-modal-img'),\n imageModalClose: document.getElementById('image-modal-close'),\n showPrompts: document.getElementById('show-prompts'),\n promptsModal: document.getElementById('prompts-modal'),\n promptsClose: document.getElementById('prompts-close'),\n promptsText: document.getElementById('prompts-text'),\n promptsCopy: document.getElementById('prompts-copy'),\n };\n\n const state = {\n sourceLabel: null,\n tzMode: 'local',\n events: [],\n filtered: [],\n selectedId: null,\n selected: null,\n filters: {\n query: '',\n type: 'all',\n contentType: 'all',\n role: 'all',\n hideThinking: false,\n },\n };\n\n let lastActiveElementBeforeModal = null;\n let lastBodyOverflow = null;\n let lastActiveElementBeforePrompts = null;\n let lastBodyOverflowBeforePrompts = null;\n let promptsCopiedTimer = null;\n let suppressUrlStateSync = false;\n const wideLayoutMq = window.matchMedia('(min-width: 1060px)');\n const detailDock = document.createElement('div');\n detailDock.id = 'detail-dock';\n detailDock.style.display = 'none';\n els.detailPanel.parentElement.insertBefore(detailDock, els.detailPanel);\n\n function updateDetailPlacement() {\n const inline = !wideLayoutMq.matches;\n\n if (!inline) {\n if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n els.detailPanel.classList.remove('inline');\n return;\n }\n\n const selectedNode = els.timeline.querySelector('.event.selected');\n if (!selectedNode) {\n if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n els.detailPanel.classList.remove('inline');\n return;\n }\n\n if (selectedNode.nextSibling !== els.detailPanel) selectedNode.after(els.detailPanel);\n els.detailPanel.classList.add('inline');\n }\n\n function setStatus(message, kind = 'good') {\n if (!message) {\n els.status.className = 'status';\n els.status.textContent = '';\n return;\n }\n els.status.className = `status visible ${kind}`;\n els.status.textContent = message;\n }\n\n function pad2(number) {\n return String(number).padStart(2, '0');\n }\n\n function pad3(number) {\n return String(number).padStart(3, '0');\n }\n\n function getOffsetString(date) {\n const offsetMinutes = -date.getTimezoneOffset();\n const sign = offsetMinutes >= 0 ? '+' : '-';\n const abs = Math.abs(offsetMinutes);\n const hours = Math.floor(abs / 60);\n const minutes = abs % 60;\n return `${sign}${pad2(hours)}:${pad2(minutes)}`;\n }\n\n function formatTimestamp(date, tzMode) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n if (tzMode === 'utc') {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +\n `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}.` +\n `${pad3(date.getUTCMilliseconds())}Z`;\n }\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ` +\n `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}.` +\n `${pad3(date.getMilliseconds())} ${getOffsetString(date)}`;\n }\n\n function dayKey(date, tzMode) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n if (tzMode === 'utc') {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;\n }\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n }\n\n function formatDelta(ms) {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return '';\n const abs = Math.abs(ms);\n const sign = ms < 0 ? '-' : '+';\n if (abs < 1000) return `${sign}${Math.round(abs)}ms`;\n if (abs < 60_000) return `${sign}${(abs / 1000).toFixed(abs < 10_000 ? 2 : 1)}s`;\n if (abs < 3_600_000) {\n const m = Math.floor(abs / 60_000);\n const s = Math.floor((abs % 60_000) / 1000);\n return `${sign}${m}m ${pad2(s)}s`;\n }\n if (abs < 86_400_000) {\n const h = Math.floor(abs / 3_600_000);\n const m = Math.floor((abs % 3_600_000) / 60_000);\n return `${sign}${h}h ${m}m`;\n }\n const d = Math.floor(abs / 86_400_000);\n const h = Math.floor((abs % 86_400_000) / 3_600_000);\n return `${sign}${d}d ${h}h`;\n }\n\n function safeJsonParse(text) {\n try {\n return { ok: true, value: JSON.parse(text) };\n } catch (error) {\n return { ok: false, error };\n }\n }\n\n function extractMessageText(message) {\n if (!message || typeof message !== 'object') return null;\n const content = message.content;\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return null;\n const parts = [];\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n if (typeof item.text === 'string') parts.push(item.text);\n else if (typeof item.thinking === 'string') parts.push(item.thinking);\n }\n const joined = parts.join('');\n return joined || null;\n }\n\n function getContentTypes(message) {\n if (!message || typeof message !== 'object') return [];\n const content = message.content;\n if (typeof content === 'string') return ['text'];\n if (!Array.isArray(content)) return [];\n const types = new Set();\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n if (item.type) types.add(item.type);\n }\n return Array.from(types);\n }\n\n function firstLine(text) {\n if (typeof text !== 'string') return null;\n const trimmed = text.trim();\n if (!trimmed) return null;\n const idx = trimmed.indexOf('\\n');\n return idx === -1 ? trimmed : trimmed.slice(0, idx).trim();\n }\n\n function normalizeSummaryText(text, maxLen = 260) {\n if (typeof text !== 'string') return '';\n const singleLine = text.replace(/\\s+/g, ' ').trim();\n if (singleLine.length <= maxLen) return singleLine;\n return singleLine.slice(0, maxLen - 1) + '...';\n }\n\n function summarizeEvent(obj) {\n const kind = obj?.type || '';\n const message = obj?.message;\n\n if (kind === 'file-history-snapshot') {\n const msgId = obj?.messageId ? `msg=${obj.messageId.slice(0, 8)}...` : null;\n return normalizeSummaryText(msgId || 'file-history-snapshot');\n }\n\n if (kind === 'user' || kind === 'assistant') {\n const role = message?.role || kind;\n const content = message?.content;\n\n if (typeof content === 'string') {\n return normalizeSummaryText(`${role}: ${content}`);\n }\n\n if (Array.isArray(content) && content.length > 0) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n return normalizeSummaryText(`${role}: ${item.text}`);\n }\n if (item?.type === 'thinking' && item.thinking) {\n return normalizeSummaryText(`${role} (thinking): ${item.thinking}`);\n }\n if (item?.type === 'tool_use') {\n const name = item.name || 'tool';\n const input = item.input;\n if (name === 'Bash' && input?.command) {\n return normalizeSummaryText(`${name}: ${input.command}`);\n }\n if (name === 'Read' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Write' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Edit' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Glob' && input?.pattern) {\n return normalizeSummaryText(`${name}: ${input.pattern}`);\n }\n if (name === 'Grep' && input?.pattern) {\n return normalizeSummaryText(`${name}: ${input.pattern}`);\n }\n if (name === 'Task' && input?.description) {\n return normalizeSummaryText(`${name}: ${input.description}`);\n }\n if (input && typeof input === 'object') {\n const keys = Object.keys(input).slice(0, 3);\n return normalizeSummaryText(`${name}(${keys.join(', ')}${Object.keys(input).length > 3 ? ', ...' : ''})`);\n }\n return normalizeSummaryText(`${name}()`);\n }\n if (item?.type === 'tool_result') {\n const toolUseId = item.tool_use_id ? item.tool_use_id.slice(0, 12) : 'result';\n const resultContent = item.content;\n if (typeof resultContent === 'string') {\n return normalizeSummaryText(`tool_result: ${firstLine(resultContent) || toolUseId}`);\n }\n if (Array.isArray(resultContent)) {\n for (const rc of resultContent) {\n if (rc?.type === 'text' && rc.text) {\n return normalizeSummaryText(`tool_result: ${firstLine(rc.text) || toolUseId}`);\n }\n if (rc?.type === 'image') {\n return normalizeSummaryText(`tool_result: [image]`);\n }\n }\n }\n return normalizeSummaryText(`tool_result: ${toolUseId}`);\n }\n if (item?.type === 'image') {\n return normalizeSummaryText(`${role}: [image]`);\n }\n }\n return normalizeSummaryText(`${role}: [${content.length} content items]`);\n }\n\n return kind;\n }\n\n return kind || 'event';\n }\n\n function buildSearchText(obj, summary) {\n const pieces = [];\n pieces.push(obj?.type || '');\n pieces.push(obj?.uuid || '');\n pieces.push(obj?.sessionId || '');\n pieces.push(obj?.cwd || '');\n pieces.push(obj?.gitBranch || '');\n pieces.push(obj?.slug || '');\n const message = obj?.message;\n if (message && typeof message === 'object') {\n pieces.push(message.role || '');\n const msg = extractMessageText(message);\n if (msg) pieces.push(msg);\n const content = message.content;\n if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'tool_use') {\n pieces.push(item.name || '');\n if (item.input && typeof item.input === 'object') {\n pieces.push(JSON.stringify(item.input).slice(0, 500));\n }\n }\n if (item?.type === 'tool_result' && typeof item.content === 'string') {\n pieces.push(item.content.slice(0, 500));\n }\n }\n }\n }\n if (typeof summary === 'string') pieces.push(summary);\n return pieces.join('\\n').toLowerCase();\n }\n\n function stringifyJson(value, truncateLongStrings) {\n const maxLen = 8000;\n return JSON.stringify(value, (key, v) => {\n if (!truncateLongStrings) return v;\n if (typeof v === 'string' && v.length > maxLen) {\n return v.slice(0, maxLen) + `... [truncated ${v.length - maxLen} chars]`;\n }\n return v;\n }, 2);\n }\n\n function updateFilterOptions(events) {\n const typeSet = new Set();\n const contentTypeSet = new Set();\n const roleSet = new Set();\n for (const e of events) {\n if (e.kind) typeSet.add(e.kind);\n for (const ct of e.contentTypes) {\n contentTypeSet.add(ct);\n }\n if (!e.contentTypes.length) contentTypeSet.add('(none)');\n if (e.role) roleSet.add(e.role);\n }\n\n function setOptions(select, items, current) {\n select.innerHTML = '';\n const optAll = document.createElement('option');\n optAll.value = 'all';\n optAll.textContent = 'All';\n select.appendChild(optAll);\n for (const item of items) {\n const opt = document.createElement('option');\n opt.value = item;\n opt.textContent = item;\n select.appendChild(opt);\n }\n select.value = current;\n }\n\n setOptions(els.typeFilter, Array.from(typeSet).sort(), state.filters.type);\n setOptions(els.contentTypeFilter, Array.from(contentTypeSet).sort(), state.filters.contentType);\n setOptions(els.roleFilter, Array.from(roleSet).sort(), state.filters.role);\n\n if (events.length) {\n state.filters.type = els.typeFilter.value || 'all';\n state.filters.contentType = els.contentTypeFilter.value || 'all';\n state.filters.role = els.roleFilter.value || 'all';\n }\n }\n\n function readHashParams() {\n const raw = window.location.hash && window.location.hash.startsWith('#')\n ? window.location.hash.slice(1)\n : '';\n return new URLSearchParams(raw);\n }\n\n function writeHashParams(params) {\n const base = window.location.href.split('#')[0];\n const hash = params.toString();\n const next = base + (hash ? `#${hash}` : '');\n window.history.replaceState(null, '', next);\n }\n\n function syncUrlHashFromControls({ clearSel = false } = {}) {\n if (suppressUrlStateSync) return;\n const params = readHashParams();\n params.set('tz', state.tzMode);\n params.set('q', (els.search.value || '').trim());\n params.set('type', state.filters.type || 'all');\n params.set('ct', state.filters.contentType || 'all');\n params.set('role', state.filters.role || 'all');\n params.set('hide', state.filters.hideThinking ? '1' : '0');\n params.set('truncate', els.truncateStrings.checked ? '1' : '0');\n const selectedLine = state.selected?.line;\n if (typeof selectedLine === 'number' && Number.isFinite(selectedLine)) {\n params.set('sel', String(selectedLine));\n } else if (clearSel) {\n params.delete('sel');\n }\n writeHashParams(params);\n }\n\n function applySelectionFromHash() {\n if (!state.events.length) return;\n const params = readHashParams();\n const raw = params.get('sel');\n if (!raw) return;\n const line = Number(raw);\n if (!Number.isFinite(line)) return;\n const found = state.events.find(e => e.line === line);\n if (!found) return;\n if (state.selectedId === found.id) return;\n suppressUrlStateSync = true;\n try {\n selectEvent(found.id, { scroll: true });\n } finally {\n suppressUrlStateSync = false;\n }\n }\n\n function applyControlsFromHash() {\n const params = readHashParams();\n suppressUrlStateSync = true;\n try {\n const q = params.get('q');\n if (q !== null) {\n els.search.value = q;\n state.filters.query = q.trim().toLowerCase();\n }\n\n const type = params.get('type');\n if (type !== null) state.filters.type = type;\n const ct = params.get('ct');\n if (ct !== null) state.filters.contentType = ct;\n const role = params.get('role');\n if (role !== null) state.filters.role = role;\n\n const hide = params.get('hide');\n if (hide !== null) {\n els.hideThinking.checked = hide === '1' || hide === 'true';\n state.filters.hideThinking = els.hideThinking.checked;\n }\n\n const truncate = params.get('truncate');\n if (truncate !== null) {\n els.truncateStrings.checked = truncate === '1' || truncate === 'true';\n }\n\n const tz = params.get('tz');\n if (tz === 'utc' || tz === 'local') {\n state.tzMode = tz;\n els.tzLocal.classList.toggle('active', tz === 'local');\n els.tzUtc.classList.toggle('active', tz === 'utc');\n }\n } finally {\n suppressUrlStateSync = false;\n }\n }\n\n function applyFilters() {\n const query = state.filters.query;\n const type = state.filters.type;\n const contentType = state.filters.contentType;\n const role = state.filters.role;\n const hideThinking = state.filters.hideThinking;\n\n const out = [];\n for (const e of state.events) {\n if (hideThinking && e.contentTypes.includes('thinking') && e.contentTypes.length === 1) continue;\n if (type !== 'all' && e.kind !== type) continue;\n if (contentType !== 'all') {\n const ct = e.contentTypes.length ? e.contentTypes : ['(none)'];\n if (!ct.includes(contentType)) continue;\n }\n if (role !== 'all' && e.role !== role) continue;\n if (query && !e.search.includes(query)) continue;\n out.push(e);\n }\n\n state.filtered = out;\n els.statsShowing.textContent = String(out.length);\n els.statsTotal.textContent = String(state.events.length);\n\n const start = out.find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n const end = [...out].reverse().find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n if (start && end) {\n els.statsRange.textContent = `${formatTimestamp(start, state.tzMode)} -> ${formatTimestamp(end, state.tzMode)}`;\n } else {\n els.statsRange.textContent = '-';\n }\n\n renderTimeline();\n }\n\n function clearSelection({ syncHash = true } = {}) {\n state.selectedId = null;\n state.selected = null;\n els.detailTitle.textContent = 'Select an event';\n renderDetailImages(null);\n renderPrettyView(null);\n els.detailJson.textContent = state.events.length ? 'Select an event from the timeline.' : 'Load a file to get started.';\n els.detailJson.classList.add('empty');\n els.copyJson.disabled = true;\n els.copyRaw.disabled = true;\n for (const node of els.timeline.querySelectorAll('.event.selected')) {\n node.classList.remove('selected');\n }\n updateDetailPlacement();\n if (syncHash) syncUrlHashFromControls({ clearSel: true });\n }\n\n function renderTimeline() {\n els.timeline.innerHTML = '';\n\n if (!state.filtered.length) {\n const empty = document.createElement('div');\n empty.className = 'empty';\n empty.textContent = state.events.length ? 'No events match your filters.' : 'Load a file to see a timeline.';\n els.timeline.appendChild(empty);\n clearSelection();\n return;\n }\n\n const fragment = document.createDocumentFragment();\n let lastDay = null;\n let prevDate = null;\n\n for (const e of state.filtered) {\n const day = dayKey(e.date, state.tzMode);\n if (day !== lastDay) {\n lastDay = day;\n const sep = document.createElement('div');\n sep.className = 'day-sep';\n sep.textContent = day;\n fragment.appendChild(sep);\n }\n\n const row = document.createElement('div');\n row.className = 'event';\n row.setAttribute('role', 'listitem');\n row.dataset.eventId = String(e.id);\n\n const time = document.createElement('div');\n time.className = 'time';\n\n const ts = document.createElement('div');\n ts.className = 'ts';\n ts.textContent = formatTimestamp(e.date, state.tzMode);\n ts.dataset.kind = 'timestamp';\n\n const meta = document.createElement('div');\n meta.textContent = [\n `#${e.line}`,\n (prevDate && e.date) ? formatDelta(e.date.getTime() - prevDate.getTime()) : null,\n ].filter(Boolean).join(' . ');\n\n time.appendChild(ts);\n time.appendChild(meta);\n\n const body = document.createElement('div');\n\n const badges = document.createElement('div');\n badges.className = 'badges';\n const badgeKind = document.createElement('span');\n badgeKind.className = `badge k-${e.kind}`;\n badgeKind.textContent = e.kind;\n badges.appendChild(badgeKind);\n\n for (const ct of e.contentTypes.slice(0, 2)) {\n const badgeCt = document.createElement('span');\n badgeCt.className = `badge ct-${ct}`;\n badgeCt.textContent = ct;\n badges.appendChild(badgeCt);\n }\n if (e.contentTypes.length > 2) {\n const badgeMore = document.createElement('span');\n badgeMore.className = 'badge';\n badgeMore.textContent = `+${e.contentTypes.length - 2}`;\n badges.appendChild(badgeMore);\n }\n\n if (e.role) {\n const badgeRole = document.createElement('span');\n badgeRole.className = `badge role-${e.role}`;\n badgeRole.textContent = e.role;\n badges.appendChild(badgeRole);\n }\n\n const summary = document.createElement('div');\n summary.className = 'summary';\n summary.textContent = e.summary;\n\n body.appendChild(badges);\n body.appendChild(summary);\n\n row.appendChild(time);\n row.appendChild(body);\n\n row.addEventListener('click', () => selectEvent(e.id, { focusTimeline: true }));\n fragment.appendChild(row);\n\n if (e.date instanceof Date && !Number.isNaN(e.date.getTime())) prevDate = e.date;\n }\n\n els.timeline.appendChild(fragment);\n\n if (state.selectedId !== null) {\n const stillThere = state.filtered.some(e => e.id === state.selectedId);\n if (!stillThere) clearSelection();\n else selectEvent(state.selectedId, { scroll: false });\n } else {\n clearSelection({ syncHash: false });\n }\n }\n\n function selectEvent(id, { scroll = false, focusTimeline = false } = {}) {\n const e = state.filtered.find(item => item.id === id) || state.events.find(item => item.id === id);\n if (!e) return;\n state.selectedId = id;\n state.selected = e;\n\n if (focusTimeline) {\n els.timeline.focus({ preventScroll: true });\n }\n\n for (const node of els.timeline.querySelectorAll('.event.selected')) {\n node.classList.remove('selected');\n }\n const node = els.timeline.querySelector(`.event[data-event-id=\"${CSS.escape(String(id))}\"]`);\n if (node) {\n node.classList.add('selected');\n updateDetailPlacement();\n if (scroll) node.scrollIntoView({ block: 'center' });\n }\n\n els.detailJson.classList.remove('empty');\n const titleParts = [\n formatTimestamp(e.date, state.tzMode),\n `#${e.line}`,\n e.kind,\n e.role ? `(${e.role})` : null,\n ].filter(Boolean);\n els.detailTitle.textContent = titleParts.join(' ');\n\n const shouldTruncate = els.truncateStrings.checked;\n renderDetailImages(e.obj);\n renderPrettyView(e.obj);\n els.detailJson.textContent = stringifyJson(e.obj, shouldTruncate) || '';\n els.copyJson.disabled = false;\n els.copyRaw.disabled = false;\n updateDetailPlacement();\n syncUrlHashFromControls();\n }\n\n function extractImageUrls(obj) {\n const urls = [];\n const message = obj?.message;\n if (!message || typeof message !== 'object') return urls;\n const content = message.content;\n if (!Array.isArray(content)) return urls;\n\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n\n // Direct image in content\n if (item.type === 'image' && item.source?.type === 'base64' && item.source?.data) {\n const mediaType = item.source.media_type || 'image/png';\n urls.push(`data:${mediaType};base64,${item.source.data}`);\n }\n\n // Image in tool_result content\n if (item.type === 'tool_result' && Array.isArray(item.content)) {\n for (const rc of item.content) {\n if (rc?.type === 'image' && rc.source?.type === 'base64' && rc.source?.data) {\n const mediaType = rc.source.media_type || 'image/png';\n urls.push(`data:${mediaType};base64,${rc.source.data}`);\n }\n }\n }\n }\n\n return urls;\n }\n\n function escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n // Initialize markdown-it with GFM-like settings\n const md = window.markdownit({\n html: false, // Disable raw HTML input\n linkify: true, // Auto-convert URLs to links\n typographer: true, // Smart quotes, dashes\n breaks: true // Convert \\n to <br>\n });\n\n function renderMarkdown(text) {\n if (typeof text !== 'string') return '';\n\n // Render markdown to HTML\n const html = md.render(text);\n\n // Sanitize with DOMPurify to prevent XSS\n return DOMPurify.sanitize(html);\n }\n\n function renderPrettyView(obj) {\n els.detailPretty.innerHTML = '';\n\n if (!obj || typeof obj !== 'object') {\n els.detailPretty.hidden = true;\n return;\n }\n\n const message = obj.message;\n if (!message || typeof message !== 'object') {\n els.detailPretty.hidden = true;\n return;\n }\n\n const content = message.content;\n const fragment = document.createDocumentFragment();\n let hasContent = false;\n\n // Handle string content (simple text message)\n if (typeof content === 'string') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = message.role || 'Message';\n const body = document.createElement('div');\n body.className = 'pretty-content';\n body.innerHTML = renderMarkdown(content);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Handle array content\n if (Array.isArray(content)) {\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n\n // Text content\n if (item.type === 'text' && item.text) {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = message.role === 'assistant' ? 'Response' : 'Message';\n const body = document.createElement('div');\n body.className = 'pretty-content';\n body.innerHTML = renderMarkdown(item.text);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Thinking content\n if (item.type === 'thinking' && item.thinking) {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = 'Thinking';\n const body = document.createElement('div');\n body.className = 'thinking-block pretty-content';\n body.innerHTML = renderMarkdown(item.thinking);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Tool use\n if (item.type === 'tool_use') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = 'Tool Call';\n\n const toolName = item.name || 'Unknown Tool';\n const input = item.input || {};\n\n // Special handling for Bash command\n if (toolName === 'Bash' && input.command) {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n if (input.description) {\n const descSpan = document.createElement('span');\n descSpan.className = 'tool-desc';\n descSpan.textContent = input.description;\n header.appendChild(descSpan);\n }\n\n const cmdInput = document.createElement('div');\n cmdInput.className = 'command-input';\n cmdInput.textContent = input.command;\n\n block.appendChild(header);\n block.appendChild(cmdInput);\n section.appendChild(label);\n section.appendChild(block);\n }\n // Special handling for file operations\n else if (['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(toolName)) {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n\n block.appendChild(header);\n\n const params = document.createElement('div');\n params.className = 'tool-params';\n params.style.padding = '10px 12px';\n\n for (const [key, value] of Object.entries(input)) {\n const param = document.createElement('div');\n param.className = 'tool-param';\n const paramName = document.createElement('span');\n paramName.className = 'tool-param-name';\n paramName.textContent = key;\n const paramValue = document.createElement('span');\n paramValue.className = 'tool-param-value';\n if (typeof value === 'string') {\n paramValue.textContent = value.length > 500 ? value.slice(0, 500) + '...' : value;\n } else {\n paramValue.textContent = JSON.stringify(value, null, 2);\n }\n param.appendChild(paramName);\n param.appendChild(paramValue);\n params.appendChild(param);\n }\n\n block.appendChild(params);\n section.appendChild(label);\n section.appendChild(block);\n }\n // Generic tool display\n else {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n\n block.appendChild(header);\n\n if (Object.keys(input).length > 0) {\n const params = document.createElement('div');\n params.className = 'tool-params';\n params.style.padding = '10px 12px';\n\n for (const [key, value] of Object.entries(input)) {\n const param = document.createElement('div');\n param.className = 'tool-param';\n const paramName = document.createElement('span');\n paramName.className = 'tool-param-name';\n paramName.textContent = key;\n const paramValue = document.createElement('span');\n paramValue.className = 'tool-param-value';\n if (typeof value === 'string') {\n paramValue.textContent = value.length > 1000 ? value.slice(0, 1000) + '...' : value;\n } else {\n paramValue.textContent = JSON.stringify(value, null, 2);\n }\n param.appendChild(paramName);\n param.appendChild(paramValue);\n params.appendChild(param);\n }\n\n block.appendChild(params);\n }\n\n section.appendChild(label);\n section.appendChild(block);\n }\n\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Tool result\n if (item.type === 'tool_result') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = item.is_error ? 'Tool Error' : 'Tool Result';\n\n const resultContent = item.content;\n let resultText = '';\n\n if (typeof resultContent === 'string') {\n resultText = resultContent;\n } else if (Array.isArray(resultContent)) {\n for (const rc of resultContent) {\n if (rc?.type === 'text' && rc.text) {\n resultText += rc.text;\n }\n }\n }\n\n if (resultText) {\n const block = document.createElement('div');\n block.className = 'pretty-content' + (item.is_error ? ' result-error' : '');\n const displayText = resultText.length > 5000 ? resultText.slice(0, 5000) + '\\n... [truncated]' : resultText;\n block.innerHTML = renderMarkdown(displayText);\n section.appendChild(label);\n section.appendChild(block);\n fragment.appendChild(section);\n hasContent = true;\n }\n }\n }\n }\n\n if (hasContent) {\n els.detailPretty.appendChild(fragment);\n els.detailPretty.hidden = false;\n } else {\n els.detailPretty.hidden = true;\n }\n }\n\n function openImageModal(src, altText = '') {\n if (!src) return;\n lastActiveElementBeforeModal = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n lastBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n\n els.imageModalImg.src = src;\n els.imageModalImg.alt = altText;\n els.imageModal.classList.add('open');\n els.imageModal.setAttribute('aria-hidden', 'false');\n els.imageModalClose.focus();\n }\n\n function closeImageModal() {\n if (!els.imageModal.classList.contains('open')) return;\n els.imageModal.classList.remove('open');\n els.imageModal.setAttribute('aria-hidden', 'true');\n els.imageModalImg.removeAttribute('src');\n els.imageModalImg.alt = '';\n document.body.style.overflow = lastBodyOverflow ?? '';\n\n if (lastActiveElementBeforeModal) {\n lastActiveElementBeforeModal.focus();\n }\n lastActiveElementBeforeModal = null;\n lastBodyOverflow = null;\n }\n\n function renderDetailImages(obj) {\n const urls = extractImageUrls(obj);\n els.detailImages.innerHTML = '';\n if (!urls.length) {\n els.detailImages.hidden = true;\n return;\n }\n const fragment = document.createDocumentFragment();\n urls.forEach((src, idx) => {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'thumb';\n btn.title = `Open image ${idx + 1}`;\n const img = document.createElement('img');\n img.loading = 'lazy';\n img.src = src;\n img.alt = `Image ${idx + 1}`;\n btn.appendChild(img);\n btn.addEventListener('click', () => openImageModal(src, `Image ${idx + 1}`));\n fragment.appendChild(btn);\n });\n els.detailImages.appendChild(fragment);\n els.detailImages.hidden = false;\n }\n\n function isEditableElement(el) {\n if (!(el instanceof HTMLElement)) return false;\n if (el.isContentEditable) return true;\n const tag = el.tagName;\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';\n }\n\n function moveSelection(delta) {\n if (!state.filtered.length) return;\n const idx = state.filtered.findIndex(e => e.id === state.selectedId);\n if (idx === -1) {\n selectEvent(state.filtered[0].id, { scroll: true });\n return;\n }\n const nextIdx = Math.max(0, Math.min(state.filtered.length - 1, idx + delta));\n if (nextIdx === idx) return;\n selectEvent(state.filtered[nextIdx].id, { scroll: true });\n }\n\n async function copyText(text) {\n if (typeof text !== 'string') return;\n try {\n await navigator.clipboard.writeText(text);\n setStatus('Copied to clipboard.', 'good');\n } catch (error) {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n document.body.removeChild(ta);\n setStatus('Copied to clipboard.', 'good');\n }\n }\n\n function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n\n const message = e.obj?.message;\n if (!message) continue;\n\n const content = message.content;\n if (typeof content === 'string') {\n prompts.push(content);\n } else if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n prompts.push(item.text);\n }\n }\n }\n }\n return prompts.join('\\n\\n');\n }\n\n function openPromptsModal(text) {\n lastActiveElementBeforePrompts = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n lastBodyOverflowBeforePrompts = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n\n els.promptsText.value = text || '';\n els.promptsModal.classList.add('open');\n els.promptsModal.setAttribute('aria-hidden', 'false');\n els.promptsClose.focus();\n els.promptsText.scrollTop = 0;\n }\n\n function closePromptsModal() {\n if (!els.promptsModal.classList.contains('open')) return;\n els.promptsModal.classList.remove('open');\n els.promptsModal.setAttribute('aria-hidden', 'true');\n els.promptsText.value = '';\n document.body.style.overflow = lastBodyOverflowBeforePrompts ?? '';\n\n if (lastActiveElementBeforePrompts) lastActiveElementBeforePrompts.focus();\n lastActiveElementBeforePrompts = null;\n lastBodyOverflowBeforePrompts = null;\n }\n\n function setTimezoneMode(mode) {\n state.tzMode = mode;\n els.tzLocal.classList.toggle('active', mode === 'local');\n els.tzUtc.classList.toggle('active', mode === 'utc');\n applyFilters();\n if (state.selectedId !== null) selectEvent(state.selectedId);\n syncUrlHashFromControls();\n }\n\n els.promptsClose.addEventListener('click', closePromptsModal);\n els.promptsModal.addEventListener('click', (event) => {\n if (event.target === els.promptsModal) closePromptsModal();\n });\n els.promptsCopy.addEventListener('click', async () => {\n const text = els.promptsText.value || '';\n if (!text.trim()) {\n setStatus('No user prompts found.', 'warn');\n return;\n }\n await copyText(text);\n if (promptsCopiedTimer) clearTimeout(promptsCopiedTimer);\n const originalText = els.promptsCopy.textContent;\n els.promptsCopy.textContent = 'Copied!';\n promptsCopiedTimer = setTimeout(() => {\n els.promptsCopy.textContent = originalText;\n promptsCopiedTimer = null;\n }, 1500);\n });\n els.showPrompts.addEventListener('click', () => {\n const text = extractUserPrompts();\n if (!text) {\n setStatus('No user prompts found.', 'warn');\n return;\n }\n openPromptsModal(text);\n });\n\n els.imageModalClose.addEventListener('click', closeImageModal);\n els.imageModal.addEventListener('click', (event) => {\n if (event.target === els.imageModal) closeImageModal();\n });\n document.addEventListener('keydown', (event) => {\n if (event.key !== 'Escape') return;\n if (els.promptsModal.classList.contains('open')) closePromptsModal();\n if (els.imageModal.classList.contains('open')) closeImageModal();\n });\n\n document.addEventListener('keydown', (event) => {\n if (els.imageModal.classList.contains('open')) return;\n if (els.promptsModal.classList.contains('open')) return;\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;\n if (isEditableElement(document.activeElement)) return;\n if (document.activeElement !== els.timeline && !els.timeline.contains(document.activeElement)) return;\n if (state.selectedId === null) return;\n event.preventDefault();\n moveSelection(event.key === 'ArrowUp' ? -1 : 1);\n });\n\n if (typeof wideLayoutMq.addEventListener === 'function') {\n wideLayoutMq.addEventListener('change', updateDetailPlacement);\n } else if (typeof wideLayoutMq.addListener === 'function') {\n wideLayoutMq.addListener(updateDetailPlacement);\n }\n\n function setTab(tabName) {\n for (const tab of document.querySelectorAll('.tab')) {\n const active = tab.dataset.tab === tabName;\n tab.classList.toggle('active', active);\n tab.setAttribute('aria-selected', active ? 'true' : 'false');\n }\n for (const panel of document.querySelectorAll('[data-panel]')) {\n panel.hidden = panel.dataset.panel !== tabName;\n }\n }\n\n function setShareUrlParam(url) {\n const current = new URL(window.location.href);\n if (url) current.searchParams.set('url', url);\n else current.searchParams.delete('url');\n window.history.replaceState(null, '', current);\n }\n\n async function fetchAndRenderUrl(url, { syncUrl = true } = {}) {\n const trimmed = (url || '').trim();\n if (!trimmed) {\n setStatus('Enter a URL first.', 'error');\n return;\n }\n if (syncUrl) setShareUrlParam(trimmed);\n setStatus(`Fetching ${trimmed}...`, 'warn');\n try {\n const resp = await fetch(trimmed, { mode: 'cors' });\n if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n const text = await resp.text();\n await loadText(text, trimmed);\n } catch (error) {\n setStatus(`Failed to fetch URL: ${String(error)}`, 'error');\n }\n }\n\n async function loadText(text, sourceLabel) {\n const started = performance.now();\n state.sourceLabel = sourceLabel || null;\n setStatus(`Parsing ${sourceLabel || 'content'}...`, 'warn');\n closePromptsModal();\n els.showPrompts.disabled = true;\n clearSelection({ syncHash: false });\n\n const lines = text.split(/\\r?\\n/);\n const events = [];\n const errors = [];\n let idCounter = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (!line.trim()) continue;\n const parsed = safeJsonParse(line);\n if (!parsed.ok) {\n errors.push({ line: i + 1, error: parsed.error });\n continue;\n }\n const obj = parsed.value;\n const ts = typeof obj.timestamp === 'string' ? new Date(obj.timestamp) : null;\n const kind = obj.type || '(unknown)';\n const message = obj.message && typeof obj.message === 'object' ? obj.message : null;\n const contentTypes = message ? getContentTypes(message) : [];\n const role = message?.role ?? null;\n\n const summary = summarizeEvent(obj);\n events.push({\n id: ++idCounter,\n line: i + 1,\n raw: line,\n obj,\n date: ts,\n kind,\n contentTypes,\n role,\n summary,\n search: buildSearchText(obj, summary),\n });\n }\n\n state.events = events;\n els.showPrompts.disabled = !events.length;\n updateFilterOptions(events);\n applyFilters();\n applySelectionFromHash();\n syncUrlHashFromControls();\n\n const durationMs = Math.round(performance.now() - started);\n if (errors.length) {\n const first = errors[0];\n setStatus(\n `Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.\\n` +\n `Could not parse ${errors.length} line(s). First error at line ${first.line}: ${String(first.error)}`,\n 'warn'\n );\n } else {\n setStatus(`Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.`, 'good');\n }\n }\n\n async function handleFile(file) {\n if (!file) return;\n setShareUrlParam(null);\n try {\n const text = await file.text();\n await loadText(text, file.name);\n } catch (error) {\n setStatus(`Failed to read file: ${String(error)}`, 'error');\n }\n }\n\n els.dropzone.addEventListener('click', () => els.fileInput.click());\n els.dropzone.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n els.fileInput.click();\n }\n });\n els.fileInput.addEventListener('change', () => handleFile(els.fileInput.files?.[0]));\n\n els.dropzone.addEventListener('dragover', (e) => {\n e.preventDefault();\n els.dropzone.classList.add('dragover');\n });\n els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('dragover'));\n els.dropzone.addEventListener('drop', (e) => {\n e.preventDefault();\n els.dropzone.classList.remove('dragover');\n const file = e.dataTransfer?.files?.[0];\n if (file) handleFile(file);\n });\n\n els.renderPaste.addEventListener('click', async () => {\n const text = els.pasteInput.value || '';\n if (!text.trim()) {\n setStatus('Paste some JSONL content first.', 'error');\n return;\n }\n setShareUrlParam(null);\n await loadText(text, 'pasted content');\n });\n\n els.fetchUrl.addEventListener('click', async () => {\n await fetchAndRenderUrl(els.urlInput.value, { syncUrl: true });\n });\n\n for (const tab of document.querySelectorAll('.tab')) {\n tab.addEventListener('click', () => setTab(tab.dataset.tab));\n }\n\n els.tzLocal.addEventListener('click', () => setTimezoneMode('local'));\n els.tzUtc.addEventListener('click', () => setTimezoneMode('utc'));\n\n els.search.addEventListener('input', () => {\n state.filters.query = els.search.value.trim().toLowerCase();\n applyFilters();\n syncUrlHashFromControls();\n });\n els.typeFilter.addEventListener('change', () => {\n state.filters.type = els.typeFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.contentTypeFilter.addEventListener('change', () => {\n state.filters.contentType = els.contentTypeFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.roleFilter.addEventListener('change', () => {\n state.filters.role = els.roleFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.hideThinking.addEventListener('change', () => {\n state.filters.hideThinking = els.hideThinking.checked;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.truncateStrings.addEventListener('change', () => {\n if (state.selectedId !== null) selectEvent(state.selectedId);\n syncUrlHashFromControls();\n });\n\n els.clearFilters.addEventListener('click', () => {\n els.search.value = '';\n state.filters.query = '';\n state.filters.type = 'all';\n state.filters.contentType = 'all';\n state.filters.role = 'all';\n state.filters.hideThinking = false;\n els.hideThinking.checked = false;\n updateFilterOptions(state.events);\n applyFilters();\n syncUrlHashFromControls();\n });\n\n els.copyJson.addEventListener('click', () => {\n if (!state.selected) return;\n copyText(stringifyJson(state.selected.obj, false));\n });\n els.copyRaw.addEventListener('click', () => {\n if (!state.selected) return;\n copyText(state.selected.raw);\n });\n\n async function initFromQuery() {\n const url = new URLSearchParams(window.location.search).get('url');\n if (!url) return;\n setTab('url');\n els.urlInput.value = url;\n await fetchAndRenderUrl(url, { syncUrl: false });\n }\n\n // Initialize select options for the empty state.\n applyControlsFromHash();\n updateFilterOptions([]);\n syncUrlHashFromControls();\n window.addEventListener('hashchange', () => {\n applyControlsFromHash();\n updateFilterOptions(state.events);\n applyFilters();\n applySelectionFromHash();\n });\n void initFromQuery();\n </script>\n</body>\n</html>\n","numLines":2439,"startLine":1,"totalLines":2439}}}
{"parentUuid":"b5525cfd-15ae-4823-8d33-bbdb905d5bc5","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01RE9RZmiB1S13tRzyojux8j","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Now I need to find the `extractUserPrompts` function and understand how it works. Looking at the code, I can see it's around line 2099-2122. Let me analyze it:\n\n```javascript\nfunction extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n\n const message = e.obj?.message;\n if (!message) continue;\n\n const content = message.content;\n if (typeof content === 'string') {\n prompts.push(content);\n } else if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n prompts.push(item.text);\n }\n }\n }\n }\n return prompts.join('\\n\\n');\n}\n```\n\nThe user wants to skip any prompts that have `\"isCompactSummary\": true` in their JSON object. Looking at the code, `e.obj` is the parsed JSON object for each event. So I need to add a check for `e.obj?.isCompactSummary === true` and skip those.\n\nI should add a condition like:\n```javascript\nif (e.obj?.isCompactSummary === true) continue;\n```\n\nThis should be added after the existing filters but before the message extraction.","signature":"EpoLCkYIChgCKkA93g2RGjCHsuNpMNdFIv/Te7N5c2az9YFEYrtrIJgcLFMWW59dXTONeN+IAjJi+5sPnrCF34R0ntWfe+AAyJ//Egxv6HCxLSVWOOOGHIcaDGXvtemWWUrrQ4hwlCIw7wnGhsuCgDPsuE8XXgb/j1QxyaL8tsPTye9jyyNtWNRGaU/jqaZwrfwP1YbqA3wFKoEK9D5naxsgYHFg7HYmUF44e4Ycw9rFFaiFAO/UgQGbLuqv7AFIvJp0/51jxAlVYhhuEyMYl9DPN5s8NG4lpLCJF2rutA+JJEigCoNXcH1M2sGSa0cJESqF2kFGSUanSwmIoQiRdeNJvGE52tNPhg/Bfba1yaBw+u8PaSHQzNefSTslRYSfmLI1FGBJXfJqMRp7Ng48Fc3rUX1FkLWcVfiLlbZiyeD8538mXjnjyECOHO+tXD1p0C6FNXN3G+QNRzR6/XeB65wZgC0RYedZk/cL7In3ko3XN26vggRiXdwtp3D+D5LJ/DOibdM88CJAtI0Rp1TwG6LraeMkMSHziKzdiWMRJAMg6SKbR9KyYMi2qv9nudHheXM80mOXl20s4JMMeAvCXQ9XONpkoPjkYBhLH38troubO12cQTNVl2W0JdlI/sDCfU8WTbcCeDc+B+NDOuFNHA/MreQlKytHzOvEMMIgpKURDPBxoV38CHxFLNnFSLVpToPAbVuIcvKRzVvu8q+rN/PxPwsk6il3aKNLmi06IOI91vAT0SGE60sEuVdhNv2uKE2FlRTz80Pb0U2gfmbDR9HbJsWSmnXfDJxOwmKozrFe6Gr/i1LkijpVGXIRlyXMA3jkNhuney9HkYc7uJom2z4r2+/HbjdQuTrHkiNRpQnyA+5k8zG38kTgaf/nQiCbJRYVf1qVVNVi9qhqu+uE3yf862aBEewgKF/jaarSk/lGj72mLEBg6sPaJtwOr+nLwsPZxi7QZqKREzNn7vC+ROT4Udm4QGXJhp6WqT3NblZExaIpH+4Jy8z5HU7yGjvGg1fdzyhvmFLHRwKLjen3B5KbPsADCobqFkAn15co762zYy6OOAZLtfFiNaUKSUfu3lRN69ckkpqT69AVWpf3VXjAoCieqKR6LEXhZ9T7Bt2vgT9GBFtadTfqPOnRMx58/I712cuxKLXyO04ABRJtK8j2Z/wiBD6XWaZbGSnZBLYMj4NajdGfKP6VnVwZaEg+w1ZI8+owtRV/eQspplEtMgeol7gBvh1yxrf2V7F2j4lf8VJrHdg3e4ZEGXdBDxScx3g25Y5Agr702aKUYbox+PIAODZqpbCfmTtAW+rGtcb8VPw5IG+y/0uf7Hlct44QgE9V8Bkuxli1f27J7GYzXEaQhc45wWhkQyBeqWidINaO3RO2uAABcsH5DYdCegYrr4PfQj7+9wMg0l+qcOhy9ZjPEdMGd/UslxlXEPiqy1+AQTY8SLHONgCMIjLP+NhGkQE9Q4/ol6rRiiIXqMv7zx77NBclf43+e2zF3353bbuW2b+5ztycsaUKHhiCC5f/Y33zPmSFdtMRHjAv2yLngbbuZtdzIRJc3k6YoFGyiv4PjocgNiBUBXZlxtDAVs/gtzGNcU68qNzrkbK2Fr8WjZME1IEVJNTloVIHVvA7X3LoHQFqorvntxtgWCHHN/sqLcHPxieX/oOoPV8ZO101AyFQrtxK1kqs8MSZxzTttshmdnyglNc9AT0BxzKm42aYyjtm7jmYx7lB7hJWjglHfOpgvyv88/A+i49YqpwDIs3+A40pJD1F1mQpYtYuWpaEhWpRRsJTytELnxnnCGNaCp8vdZo1cSkjrxlsG1SWLJa8S1p6LnAL38QN+oaked8Y4B7Jf1XCtnQyZhzdBoJliTBA8XwNh5QF1mNeWdPTSaQFu+TYYKodAtp4GEXWGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":34199,"cache_read_input_tokens":18944,"cache_creation":{"ephemeral_5m_input_tokens":34199,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CWFQqUVCDroNUBESCBkfj","type":"assistant","uuid":"f18082e7-7bd5-4c83-8290-de9ac5850ff3","timestamp":"2025-12-19T04:26:19.685Z"}
{"parentUuid":"f18082e7-7bd5-4c83-8290-de9ac5850ff3","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01RE9RZmiB1S13tRzyojux8j","type":"message","role":"assistant","content":[{"type":"text","text":"I found the `extractUserPrompts` function at line 2099. It currently filters for user prompts but doesn't check for `isCompactSummary`. I'll add that check."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":34199,"cache_read_input_tokens":18944,"cache_creation":{"ephemeral_5m_input_tokens":34199,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CWFQqUVCDroNUBESCBkfj","type":"assistant","uuid":"9d87afe4-56fe-43a1-9811-f018f3051f6f","timestamp":"2025-12-19T04:26:20.736Z"}
{"parentUuid":"9d87afe4-56fe-43a1-9811-f018f3051f6f","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01RE9RZmiB1S13tRzyojux8j","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01J3xaK1evzzc51pYj8vcH1b","name":"Edit","input":{"file_path":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html","old_string":" function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n\n const message = e.obj?.message;\n if (!message) continue;","new_string":" function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n // Skip compact summary messages (auto-generated context summaries)\n if (e.obj?.isCompactSummary === true) continue;\n\n const message = e.obj?.message;\n if (!message) continue;"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":34199,"cache_read_input_tokens":18944,"cache_creation":{"ephemeral_5m_input_tokens":34199,"ephemeral_1h_input_tokens":0},"output_tokens":787,"service_tier":"standard"}},"requestId":"req_011CWFQqUVCDroNUBESCBkfj","type":"assistant","uuid":"9dc10699-fe75-4ad4-9278-780a75e369b3","timestamp":"2025-12-19T04:26:23.962Z"}
{"type":"file-history-snapshot","messageId":"9dc10699-fe75-4ad4-9278-780a75e369b3","snapshot":{"messageId":"662cbb96-5dbb-415a-9dd3-8b2275c5f1b0","trackedFileBackups":{"claude-code-timeline.html":{"backupFileName":"cc3a5887c9685f80@v1","version":1,"backupTime":"2025-12-19T04:26:24.003Z"}},"timestamp":"2025-12-19T04:25:55.259Z"},"isSnapshotUpdate":true}
{"parentUuid":"9dc10699-fe75-4ad4-9278-780a75e369b3","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01J3xaK1evzzc51pYj8vcH1b","type":"tool_result","content":"The file /Users/simon/Dropbox/dev/tools/claude-code-timeline.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n 2095→ setStatus('Copied to clipboard.', 'good');\n 2096→ }\n 2097→ }\n 2098→\n 2099→ function extractUserPrompts() {\n 2100→ const prompts = [];\n 2101→ for (const e of state.events) {\n 2102→ // Filter: type=user, content-type includes text, role=user\n 2103→ if (e.kind !== 'user') continue;\n 2104→ if (e.role !== 'user') continue;\n 2105→ if (!e.contentTypes.includes('text')) continue;\n 2106→ // Skip compact summary messages (auto-generated context summaries)\n 2107→ if (e.obj?.isCompactSummary === true) continue;\n 2108→\n 2109→ const message = e.obj?.message;\n 2110→ if (!message) continue;\n 2111→\n 2112→ const content = message.content;\n 2113→ if (typeof content === 'string') {\n 2114→ prompts.push(content);"}]},"uuid":"c0c69e20-7bb6-4612-8685-f973e74d16e7","timestamp":"2025-12-19T04:26:23.995Z","toolUseResult":{"filePath":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html","oldString":" function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n\n const message = e.obj?.message;\n if (!message) continue;","newString":" function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n // Skip compact summary messages (auto-generated context summaries)\n if (e.obj?.isCompactSummary === true) continue;\n\n const message = e.obj?.message;\n if (!message) continue;","originalFile":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Claude Code Timeline Viewer</title>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.3/purify.min.js\" integrity=\"sha384-osZDKVu4ipZP703HmPOhWdyBajcFyjX2Psjk//TG1Rc0AdwEtuToaylrmcK3LdAl\" crossorigin=\"anonymous\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/markdown-it/4.4.0/markdown-it.min.js\" integrity=\"sha384-cxGp+QDeMxACP73aA0Lj0Sc/h4z1X73+umcvCyI0Vbyh3OYxgrPI+0mxG9slsMTN\" crossorigin=\"anonymous\"></script>\n <style>\n :root {\n color-scheme: light dark;\n --bg: #0b0c0f;\n --panel: #12141a;\n --panel-2: #0f1116;\n --text: #e8eaf0;\n --muted: #a6adbf;\n --line: rgba(255, 255, 255, 0.10);\n --line-2: rgba(255, 255, 255, 0.16);\n --accent: #6aa9ff;\n --accent-2: #8a5cff;\n --good: #46d39a;\n --warn: #ffcc66;\n --bad: #ff6b6b;\n --shadow: 0 18px 50px rgba(0, 0, 0, 0.35);\n --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,\n \"Apple Color Emoji\", \"Segoe UI Emoji\";\n }\n\n @media (prefers-color-scheme: light) {\n :root {\n --bg: #f7f8fc;\n --panel: #ffffff;\n --panel-2: #ffffff;\n --text: #10121a;\n --muted: #5b647a;\n --line: rgba(16, 18, 26, 0.12);\n --line-2: rgba(16, 18, 26, 0.18);\n --shadow: 0 18px 50px rgba(16, 18, 26, 0.10);\n }\n }\n\n * { box-sizing: border-box; }\n html, body { height: 100%; }\n body {\n margin: 0;\n font-family: var(--sans);\n background: radial-gradient(1200px 600px at 10% -10%, rgba(106, 169, 255, 0.18), transparent 45%),\n radial-gradient(1200px 600px at 90% -20%, rgba(138, 92, 255, 0.16), transparent 50%),\n var(--bg);\n color: var(--text);\n }\n\n .wrap {\n max-width: none;\n margin: 0;\n padding: 22px 16px 42px;\n }\n\n header {\n display: grid;\n gap: 10px;\n margin-bottom: 14px;\n }\n\n h1 {\n margin: 0;\n font-size: clamp(22px, 3vw, 32px);\n letter-spacing: 0.2px;\n }\n\n .sub {\n margin: 6px 0 0;\n color: var(--muted);\n line-height: 1.35;\n max-width: 70ch;\n }\n\n .controls {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n align-items: center;\n justify-content: flex-start;\n }\n\n .controls .field {\n display: grid;\n gap: 4px;\n min-width: 180px;\n }\n\n .label {\n font-size: 12px;\n color: var(--muted);\n }\n\n input[type=\"search\"], input[type=\"url\"], select, textarea {\n font: inherit;\n color: inherit;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n border-radius: 10px;\n padding: 10px 11px;\n outline: none;\n }\n\n input[type=\"search\"]:focus, input[type=\"url\"]:focus, select:focus, textarea:focus {\n border-color: color-mix(in srgb, var(--accent) 60%, var(--line));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .segmented {\n display: inline-flex;\n border: 1px solid var(--line);\n border-radius: 999px;\n overflow: hidden;\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n }\n\n .segmented button {\n border: none;\n padding: 9px 12px;\n font: inherit;\n background: transparent;\n color: var(--muted);\n cursor: pointer;\n min-width: 74px;\n }\n\n .segmented button.active {\n background: color-mix(in srgb, var(--accent) 16%, transparent);\n color: var(--text);\n }\n\n .segmented button:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);\n outline-offset: -2px;\n }\n\n .btn {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n color: var(--text);\n border-radius: 10px;\n padding: 10px 12px;\n font: inherit;\n cursor: pointer;\n transition: transform 80ms ease, border-color 120ms ease;\n }\n\n .btn:hover { border-color: var(--line-2); }\n .btn:active { transform: translateY(1px); }\n .btn.primary {\n border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n background: color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .btn:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n }\n\n .panel {\n background: var(--panel);\n border: 1px solid var(--line);\n border-radius: 16px;\n box-shadow: var(--shadow);\n }\n\n .input-panel {\n padding: 14px;\n margin-bottom: 14px;\n }\n\n .filters-panel {\n padding: 14px;\n margin-bottom: 14px;\n }\n\n .input-grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 14px;\n align-items: stretch;\n }\n\n @media (min-width: 920px) {\n .input-grid {\n grid-template-columns: 380px 1fr;\n }\n }\n\n .dropzone {\n border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--line));\n border-radius: 14px;\n background: linear-gradient(\n 135deg,\n color-mix(in srgb, var(--accent) 12%, transparent),\n transparent 45%,\n color-mix(in srgb, var(--accent-2) 10%, transparent)\n );\n padding: 16px;\n display: grid;\n gap: 6px;\n cursor: pointer;\n min-height: 124px;\n align-content: center;\n }\n\n .dropzone.dragover {\n border-style: solid;\n border-color: color-mix(in srgb, var(--accent) 70%, var(--line));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .dropzone strong { font-size: 15px; }\n .muted { color: var(--muted); }\n .tiny { font-size: 12px; color: var(--muted); }\n\n .tabs {\n display: flex;\n gap: 8px;\n margin-bottom: 10px;\n flex-wrap: wrap;\n align-items: center;\n }\n\n .tab {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n color: var(--muted);\n border-radius: 999px;\n padding: 7px 10px;\n cursor: pointer;\n font: inherit;\n }\n\n .tab.active {\n color: var(--text);\n border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n }\n\n textarea {\n width: 100%;\n min-height: 190px;\n resize: vertical;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n }\n\n .row {\n display: flex;\n gap: 10px;\n flex-wrap: wrap;\n align-items: center;\n margin-top: 10px;\n }\n\n .status {\n margin-top: 12px;\n padding: 10px 12px;\n border-radius: 12px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n display: none;\n white-space: pre-wrap;\n word-break: break-word;\n }\n .status.visible { display: block; }\n .status.error { border-color: color-mix(in srgb, var(--bad) 60%, var(--line)); }\n .status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n .status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n\n main {\n display: grid;\n gap: 14px;\n grid-template-columns: 1fr;\n min-height: 560px;\n }\n\n @media (min-width: 1060px) {\n main {\n grid-template-columns: 1fr 520px;\n align-items: stretch;\n }\n }\n\n .timeline-panel {\n display: grid;\n grid-template-rows: auto 1fr;\n min-height: 560px;\n }\n\n .stats {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n }\n\n .stats .big {\n font-size: 14px;\n font-weight: 600;\n }\n\n .timeline {\n overflow: auto;\n padding: 8px 0;\n }\n\n .timeline:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);\n outline-offset: -2px;\n border-radius: 14px;\n }\n\n .day-sep {\n position: sticky;\n top: 0;\n z-index: 2;\n padding: 8px 14px;\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n border-top: 1px solid var(--line);\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n color: var(--muted);\n backdrop-filter: blur(6px);\n }\n\n .event {\n display: grid;\n grid-template-columns: 176px 1fr;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid color-mix(in srgb, var(--line) 85%, transparent);\n cursor: pointer;\n }\n\n .event:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }\n .event.selected {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);\n outline-offset: -2px;\n }\n\n .time {\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.35;\n color: var(--muted);\n display: grid;\n gap: 3px;\n }\n\n .time .ts {\n color: var(--text);\n font-weight: 600;\n }\n\n .badges {\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n align-items: center;\n margin-bottom: 6px;\n }\n\n .badge {\n font-size: 11px;\n font-family: var(--mono);\n padding: 2px 7px;\n border-radius: 999px;\n border: 1px solid var(--line);\n color: var(--muted);\n background: color-mix(in srgb, var(--panel-2) 90%, transparent);\n }\n\n .badge.k-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n .badge.k-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n .badge.k-file-history-snapshot { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); color: color-mix(in srgb, var(--warn) 75%, var(--text)); }\n\n .badge.ct-text { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); color: color-mix(in srgb, var(--good) 75%, var(--text)); }\n .badge.ct-thinking { border-color: color-mix(in srgb, var(--accent-2) 55%, var(--line)); color: color-mix(in srgb, var(--accent-2) 75%, var(--text)); }\n .badge.ct-tool_use { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); color: color-mix(in srgb, var(--accent) 75%, var(--text)); }\n .badge.ct-tool_result { border-color: color-mix(in srgb, #8690a8 55%, var(--line)); color: color-mix(in srgb, #8690a8 70%, var(--text)); }\n .badge.ct-image { border-color: color-mix(in srgb, #ff9f43 55%, var(--line)); color: color-mix(in srgb, #ff9f43 75%, var(--text)); }\n\n .badge.role-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n .badge.role-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n\n .summary {\n font-size: 13px;\n line-height: 1.35;\n color: var(--text);\n word-break: break-word;\n }\n\n .summary code {\n font-family: var(--mono);\n font-size: 12px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n padding: 2px 5px;\n border-radius: 7px;\n }\n\n .detail {\n display: grid;\n grid-template-rows: auto auto auto auto 1fr;\n min-height: 560px;\n }\n\n @media (min-width: 1060px) {\n .detail {\n position: sticky;\n top: 16px;\n align-self: start;\n max-height: calc(100vh - 32px);\n }\n }\n\n .detail.inline {\n position: static;\n top: auto;\n align-self: auto;\n max-height: none;\n min-height: 0;\n margin: 8px 14px 12px;\n box-shadow: none;\n }\n\n .detail.inline pre {\n max-height: 52vh;\n }\n\n .detail-head {\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n display: grid;\n gap: 8px;\n }\n\n .detail-title {\n font-size: 14px;\n font-weight: 700;\n line-height: 1.25;\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .detail-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-start;\n }\n\n .check {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--muted);\n font-size: 12px;\n user-select: none;\n }\n\n pre {\n margin: 0;\n padding: 12px 14px 16px;\n overflow: auto;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.45;\n color: color-mix(in srgb, var(--text) 92%, transparent);\n background: transparent;\n white-space: pre-wrap;\n word-break: break-word;\n }\n\n .detail-images {\n display: flex;\n gap: 10px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n overflow-x: auto;\n overscroll-behavior-x: contain;\n }\n\n .thumb {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n border-radius: 12px;\n padding: 0;\n cursor: pointer;\n width: 78px;\n height: 58px;\n overflow: hidden;\n flex: 0 0 auto;\n display: grid;\n place-items: stretch;\n }\n\n .thumb:hover { border-color: var(--line-2); }\n\n .thumb img {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: cover;\n }\n\n .image-modal {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: rgba(0, 0, 0, 0.68);\n display: none;\n padding: 18px;\n place-items: center;\n }\n\n .image-modal.open { display: grid; }\n\n .image-modal .dialog {\n position: relative;\n border-radius: 16px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 98%, transparent);\n box-shadow: var(--shadow);\n padding: 14px;\n max-width: min(1200px, 96vw);\n max-height: 92vh;\n display: grid;\n place-items: center;\n }\n\n .image-modal img {\n display: block;\n max-width: min(1140px, 92vw);\n max-height: 84vh;\n width: auto;\n height: auto;\n object-fit: contain;\n border-radius: 10px;\n background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n }\n\n .image-modal .close {\n position: absolute;\n top: 10px;\n right: 10px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n color: var(--text);\n border-radius: 999px;\n padding: 8px 10px;\n font: inherit;\n cursor: pointer;\n }\n\n .image-modal .close:hover { border-color: var(--line-2); }\n\n .empty {\n padding: 14px;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.35;\n }\n\n /* Pretty view styles */\n .detail-pretty {\n padding: 14px;\n border-bottom: 1px solid var(--line);\n overflow: auto;\n max-height: 50vh;\n }\n\n .detail.inline .detail-pretty {\n max-height: 40vh;\n }\n\n .pretty-section {\n margin-bottom: 14px;\n }\n\n .pretty-section:last-child {\n margin-bottom: 0;\n }\n\n .pretty-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--muted);\n margin-bottom: 6px;\n }\n\n .pretty-content {\n font-size: 13px;\n line-height: 1.5;\n }\n\n .pretty-content p {\n margin: 0 0 10px;\n }\n\n .pretty-content p:last-child {\n margin-bottom: 0;\n }\n\n .pretty-content h1, .pretty-content h2, .pretty-content h3,\n .pretty-content h4, .pretty-content h5, .pretty-content h6 {\n margin: 14px 0 8px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .pretty-content h1:first-child, .pretty-content h2:first-child,\n .pretty-content h3:first-child {\n margin-top: 0;\n }\n\n .pretty-content h1 { font-size: 1.4em; }\n .pretty-content h2 { font-size: 1.25em; }\n .pretty-content h3 { font-size: 1.1em; }\n\n .pretty-content ul, .pretty-content ol {\n margin: 8px 0;\n padding-left: 1.5em;\n list-style-position: outside;\n }\n\n .pretty-content li {\n margin: 4px 0;\n }\n\n .thinking-block ul, .thinking-block ol {\n padding-left: 2em;\n }\n\n .pretty-content code {\n font-family: var(--mono);\n font-size: 0.9em;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n padding: 2px 5px;\n border-radius: 5px;\n }\n\n .pretty-content pre {\n margin: 10px 0;\n padding: 12px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n overflow-x: auto;\n }\n\n .pretty-content pre code {\n background: none;\n border: none;\n padding: 0;\n font-size: 12px;\n }\n\n .pretty-content blockquote {\n margin: 10px 0;\n padding: 8px 12px;\n border-left: 3px solid var(--accent);\n background: color-mix(in srgb, var(--accent) 8%, transparent);\n border-radius: 0 8px 8px 0;\n }\n\n .pretty-content a {\n color: var(--accent);\n text-decoration: none;\n }\n\n .pretty-content a:hover {\n text-decoration: underline;\n }\n\n .pretty-content strong {\n font-weight: 600;\n }\n\n .pretty-content em {\n font-style: italic;\n }\n\n .pretty-content table {\n border-collapse: collapse;\n margin: 10px 0;\n width: 100%;\n font-size: 13px;\n }\n\n .pretty-content th,\n .pretty-content td {\n border: 1px solid var(--line);\n padding: 8px 10px;\n text-align: left;\n }\n\n .pretty-content th {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n font-weight: 600;\n }\n\n .pretty-content tr:hover {\n background: color-mix(in srgb, var(--accent) 6%, transparent);\n }\n\n .command-block {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n overflow: hidden;\n margin: 8px 0;\n }\n\n .command-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n }\n\n .command-header .tool-name {\n font-weight: 600;\n color: var(--accent);\n }\n\n .command-header .tool-desc {\n color: var(--muted);\n }\n\n .command-input {\n padding: 10px 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n border-bottom: 1px solid var(--line);\n }\n\n .command-input::before {\n content: '$ ';\n color: var(--good);\n font-weight: 600;\n }\n\n .command-output {\n padding: 10px 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 300px;\n overflow: auto;\n color: var(--muted);\n }\n\n .file-path {\n font-family: var(--mono);\n font-size: 12px;\n padding: 6px 10px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 8px;\n display: inline-block;\n margin: 4px 0;\n }\n\n .tool-params {\n display: grid;\n gap: 8px;\n margin-top: 8px;\n }\n\n .tool-param {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 8px;\n align-items: start;\n font-size: 12px;\n }\n\n .tool-param-name {\n font-family: var(--mono);\n font-weight: 600;\n color: var(--accent);\n padding: 2px 6px;\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-radius: 4px;\n }\n\n .tool-param-value {\n font-family: var(--mono);\n white-space: pre-wrap;\n word-break: break-word;\n padding: 2px 0;\n }\n\n .thinking-block {\n background: color-mix(in srgb, var(--accent-2) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--accent-2) 30%, var(--line));\n border-radius: 10px;\n padding: 12px 16px;\n font-size: 13px;\n line-height: 1.5;\n }\n\n .result-block {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n padding: 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 400px;\n overflow: auto;\n }\n\n .result-error {\n border-color: color-mix(in srgb, var(--bad) 50%, var(--line));\n background: color-mix(in srgb, var(--bad) 8%, transparent);\n }\n\n .json-toggle {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n border-bottom: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 50%, transparent);\n }\n\n .json-toggle-label {\n font-size: 12px;\n color: var(--muted);\n font-weight: 500;\n }\n\n .prompts-modal {\n position: fixed;\n inset: 0;\n z-index: 80;\n background: rgba(0, 0, 0, 0.68);\n display: none;\n padding: 18px;\n place-items: center;\n }\n\n .prompts-modal.open { display: grid; }\n\n .prompts-modal .dialog {\n position: relative;\n border-radius: 16px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 98%, transparent);\n box-shadow: var(--shadow);\n padding: 14px;\n width: min(1100px, 96vw);\n height: min(86vh, 780px);\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: 10px;\n }\n\n .prompts-head {\n display: flex;\n gap: 10px;\n align-items: center;\n justify-content: space-between;\n }\n\n .prompts-title {\n font-weight: 700;\n letter-spacing: 0.2px;\n }\n\n .prompts-modal textarea {\n width: 100%;\n height: 100%;\n resize: none;\n border-radius: 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.45;\n padding: 12px;\n }\n\n .prompts-actions {\n display: flex;\n gap: 10px;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n }\n </style>\n</head>\n <body>\n <div class=\"wrap\">\n <header>\n <div>\n <h1>Claude Code Timeline Viewer</h1>\n <p class=\"sub\">Open a Claude Code session <code>.jsonl</code> file and explore it as an interactive timeline. Times are shown in your local timezone by default. Find sessions in <code>~/.claude/projects/</code>.</p>\n </div>\n </header>\n\n <section class=\"panel input-panel\" aria-label=\"Input\">\n <div class=\"input-grid\">\n <div>\n <div id=\"dropzone\" class=\"dropzone\" tabindex=\"0\">\n <strong>Drop a <code>.jsonl</code> file here</strong>\n <div class=\"muted\">or click to choose a file</div>\n <div class=\"tiny\">Supported: file picker, drag-and-drop, paste, or URL fetch (CORS-enabled).</div>\n <input id=\"file-input\" type=\"file\" accept=\".jsonl,application/json,text/plain\" hidden />\n </div>\n </div>\n <div>\n <div class=\"tabs\" role=\"tablist\" aria-label=\"Input tabs\">\n <button type=\"button\" class=\"tab active\" data-tab=\"paste\" aria-selected=\"true\">Paste</button>\n <button type=\"button\" class=\"tab\" data-tab=\"url\" aria-selected=\"false\">URL</button>\n </div>\n\n <div id=\"panel-paste\" data-panel=\"paste\">\n <textarea id=\"paste-input\" placeholder=\"Paste JSONL content here...\"></textarea>\n <div class=\"row\">\n <button type=\"button\" class=\"btn primary\" id=\"render-paste\">Render pasted content</button>\n <div class=\"tiny\">Tip: the timeline uses one JSON object per line.</div>\n </div>\n </div>\n\n <div id=\"panel-url\" data-panel=\"url\" hidden>\n <input id=\"url-input\" type=\"url\" placeholder=\"https://example.com/session.jsonl\" />\n <div class=\"row\">\n <button type=\"button\" class=\"btn primary\" id=\"fetch-url\">Fetch and render</button>\n <div class=\"tiny\">The URL must allow cross-origin requests (CORS).</div>\n </div>\n </div>\n </div>\n </div>\n\n <div id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></div>\n </section>\n\n <section class=\"panel filters-panel\" aria-label=\"Filters\">\n <div class=\"controls\" aria-label=\"Timeline controls\">\n <div class=\"field\" style=\"min-width: 170px\">\n <div class=\"label\">Timezone</div>\n <div class=\"segmented\" role=\"group\" aria-label=\"Timezone toggle\">\n <button type=\"button\" id=\"tz-local\" class=\"active\">Local</button>\n <button type=\"button\" id=\"tz-utc\">UTC</button>\n </div>\n </div>\n <div class=\"field\" style=\"min-width: 220px\">\n <div class=\"label\">Search</div>\n <input id=\"search\" type=\"search\" placeholder=\"message, tool name, cwd...\" autocomplete=\"off\" />\n </div>\n <div class=\"field\" style=\"min-width: 190px\">\n <div class=\"label\">Type</div>\n <select id=\"type-filter\"></select>\n </div>\n <div class=\"field\" style=\"min-width: 210px\">\n <div class=\"label\">Content Type</div>\n <select id=\"content-type-filter\"></select>\n </div>\n <div class=\"field\" style=\"min-width: 160px\">\n <div class=\"label\">Role</div>\n <select id=\"role-filter\"></select>\n </div>\n <label class=\"check\" title=\"Hide thinking blocks (can be verbose)\">\n <input type=\"checkbox\" id=\"hide-thinking\" />\n Hide thinking\n </label>\n <button type=\"button\" class=\"btn\" id=\"clear-filters\">Clear</button>\n </div>\n </section>\n\n <div class=\"controls\" style=\"margin-bottom: 14px;\">\n <button type=\"button\" class=\"btn\" id=\"show-prompts\" disabled>Show prompts</button>\n </div>\n\n <main>\n <section class=\"panel timeline-panel\" aria-label=\"Timeline\">\n <div class=\"stats\">\n <div class=\"big\"><span id=\"stats-showing\">0</span> <span class=\"muted\">showing</span> <span class=\"muted\">/</span> <span id=\"stats-total\">0</span> <span class=\"muted\">events</span></div>\n <div class=\"muted\" id=\"stats-range\">-</div>\n </div>\n <div id=\"timeline\" class=\"timeline\" role=\"list\" tabindex=\"0\" aria-label=\"Events list\"></div>\n </section>\n\n <aside class=\"panel detail\" aria-label=\"Details\">\n <div class=\"detail-head\">\n <div class=\"detail-title\" id=\"detail-title\">Select an event</div>\n <div class=\"detail-actions\">\n <button type=\"button\" class=\"btn\" id=\"copy-json\" disabled>Copy JSON</button>\n <button type=\"button\" class=\"btn\" id=\"copy-raw\" disabled>Copy raw line</button>\n <label class=\"check\" title=\"Truncate very long strings in the JSON preview\">\n <input type=\"checkbox\" id=\"truncate-strings\" checked />\n Truncate long strings\n </label>\n </div>\n </div>\n <div id=\"detail-images\" class=\"detail-images\" hidden aria-label=\"Event images\"></div>\n <div id=\"detail-pretty\" class=\"detail-pretty\" hidden aria-label=\"Formatted event view\"></div>\n <div class=\"json-toggle\">\n <span class=\"json-toggle-label\">Raw JSON</span>\n </div>\n <pre id=\"detail-json\" aria-label=\"Selected event JSON\" class=\"empty\">Load a file to get started.</pre>\n </aside>\n </main>\n </div>\n\n <div id=\"image-modal\" class=\"image-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\">\n <div class=\"dialog\">\n <button type=\"button\" class=\"close\" id=\"image-modal-close\" aria-label=\"Close image\">Close</button>\n <img id=\"image-modal-img\" alt=\"\" />\n </div>\n </div>\n\n <div id=\"prompts-modal\" class=\"prompts-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\" aria-label=\"User prompts\">\n <div class=\"dialog\">\n <div class=\"prompts-head\">\n <div class=\"prompts-title\">User prompts</div>\n <button type=\"button\" class=\"btn\" id=\"prompts-close\" aria-label=\"Close prompts\">Close</button>\n </div>\n <textarea id=\"prompts-text\" readonly spellcheck=\"false\"></textarea>\n <div class=\"prompts-actions\">\n <button type=\"button\" class=\"btn primary\" id=\"prompts-copy\">Copy</button>\n </div>\n </div>\n </div>\n\n <script>\n const els = {\n dropzone: document.getElementById('dropzone'),\n fileInput: document.getElementById('file-input'),\n pasteInput: document.getElementById('paste-input'),\n renderPaste: document.getElementById('render-paste'),\n urlInput: document.getElementById('url-input'),\n fetchUrl: document.getElementById('fetch-url'),\n status: document.getElementById('status'),\n timeline: document.getElementById('timeline'),\n detailPanel: document.querySelector('aside.detail'),\n detailTitle: document.getElementById('detail-title'),\n detailImages: document.getElementById('detail-images'),\n detailPretty: document.getElementById('detail-pretty'),\n detailJson: document.getElementById('detail-json'),\n copyJson: document.getElementById('copy-json'),\n copyRaw: document.getElementById('copy-raw'),\n truncateStrings: document.getElementById('truncate-strings'),\n tzLocal: document.getElementById('tz-local'),\n tzUtc: document.getElementById('tz-utc'),\n search: document.getElementById('search'),\n typeFilter: document.getElementById('type-filter'),\n contentTypeFilter: document.getElementById('content-type-filter'),\n roleFilter: document.getElementById('role-filter'),\n hideThinking: document.getElementById('hide-thinking'),\n statsShowing: document.getElementById('stats-showing'),\n statsTotal: document.getElementById('stats-total'),\n statsRange: document.getElementById('stats-range'),\n clearFilters: document.getElementById('clear-filters'),\n imageModal: document.getElementById('image-modal'),\n imageModalImg: document.getElementById('image-modal-img'),\n imageModalClose: document.getElementById('image-modal-close'),\n showPrompts: document.getElementById('show-prompts'),\n promptsModal: document.getElementById('prompts-modal'),\n promptsClose: document.getElementById('prompts-close'),\n promptsText: document.getElementById('prompts-text'),\n promptsCopy: document.getElementById('prompts-copy'),\n };\n\n const state = {\n sourceLabel: null,\n tzMode: 'local',\n events: [],\n filtered: [],\n selectedId: null,\n selected: null,\n filters: {\n query: '',\n type: 'all',\n contentType: 'all',\n role: 'all',\n hideThinking: false,\n },\n };\n\n let lastActiveElementBeforeModal = null;\n let lastBodyOverflow = null;\n let lastActiveElementBeforePrompts = null;\n let lastBodyOverflowBeforePrompts = null;\n let promptsCopiedTimer = null;\n let suppressUrlStateSync = false;\n const wideLayoutMq = window.matchMedia('(min-width: 1060px)');\n const detailDock = document.createElement('div');\n detailDock.id = 'detail-dock';\n detailDock.style.display = 'none';\n els.detailPanel.parentElement.insertBefore(detailDock, els.detailPanel);\n\n function updateDetailPlacement() {\n const inline = !wideLayoutMq.matches;\n\n if (!inline) {\n if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n els.detailPanel.classList.remove('inline');\n return;\n }\n\n const selectedNode = els.timeline.querySelector('.event.selected');\n if (!selectedNode) {\n if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n els.detailPanel.classList.remove('inline');\n return;\n }\n\n if (selectedNode.nextSibling !== els.detailPanel) selectedNode.after(els.detailPanel);\n els.detailPanel.classList.add('inline');\n }\n\n function setStatus(message, kind = 'good') {\n if (!message) {\n els.status.className = 'status';\n els.status.textContent = '';\n return;\n }\n els.status.className = `status visible ${kind}`;\n els.status.textContent = message;\n }\n\n function pad2(number) {\n return String(number).padStart(2, '0');\n }\n\n function pad3(number) {\n return String(number).padStart(3, '0');\n }\n\n function getOffsetString(date) {\n const offsetMinutes = -date.getTimezoneOffset();\n const sign = offsetMinutes >= 0 ? '+' : '-';\n const abs = Math.abs(offsetMinutes);\n const hours = Math.floor(abs / 60);\n const minutes = abs % 60;\n return `${sign}${pad2(hours)}:${pad2(minutes)}`;\n }\n\n function formatTimestamp(date, tzMode) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n if (tzMode === 'utc') {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +\n `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}.` +\n `${pad3(date.getUTCMilliseconds())}Z`;\n }\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ` +\n `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}.` +\n `${pad3(date.getMilliseconds())} ${getOffsetString(date)}`;\n }\n\n function dayKey(date, tzMode) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n if (tzMode === 'utc') {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;\n }\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n }\n\n function formatDelta(ms) {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return '';\n const abs = Math.abs(ms);\n const sign = ms < 0 ? '-' : '+';\n if (abs < 1000) return `${sign}${Math.round(abs)}ms`;\n if (abs < 60_000) return `${sign}${(abs / 1000).toFixed(abs < 10_000 ? 2 : 1)}s`;\n if (abs < 3_600_000) {\n const m = Math.floor(abs / 60_000);\n const s = Math.floor((abs % 60_000) / 1000);\n return `${sign}${m}m ${pad2(s)}s`;\n }\n if (abs < 86_400_000) {\n const h = Math.floor(abs / 3_600_000);\n const m = Math.floor((abs % 3_600_000) / 60_000);\n return `${sign}${h}h ${m}m`;\n }\n const d = Math.floor(abs / 86_400_000);\n const h = Math.floor((abs % 86_400_000) / 3_600_000);\n return `${sign}${d}d ${h}h`;\n }\n\n function safeJsonParse(text) {\n try {\n return { ok: true, value: JSON.parse(text) };\n } catch (error) {\n return { ok: false, error };\n }\n }\n\n function extractMessageText(message) {\n if (!message || typeof message !== 'object') return null;\n const content = message.content;\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return null;\n const parts = [];\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n if (typeof item.text === 'string') parts.push(item.text);\n else if (typeof item.thinking === 'string') parts.push(item.thinking);\n }\n const joined = parts.join('');\n return joined || null;\n }\n\n function getContentTypes(message) {\n if (!message || typeof message !== 'object') return [];\n const content = message.content;\n if (typeof content === 'string') return ['text'];\n if (!Array.isArray(content)) return [];\n const types = new Set();\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n if (item.type) types.add(item.type);\n }\n return Array.from(types);\n }\n\n function firstLine(text) {\n if (typeof text !== 'string') return null;\n const trimmed = text.trim();\n if (!trimmed) return null;\n const idx = trimmed.indexOf('\\n');\n return idx === -1 ? trimmed : trimmed.slice(0, idx).trim();\n }\n\n function normalizeSummaryText(text, maxLen = 260) {\n if (typeof text !== 'string') return '';\n const singleLine = text.replace(/\\s+/g, ' ').trim();\n if (singleLine.length <= maxLen) return singleLine;\n return singleLine.slice(0, maxLen - 1) + '...';\n }\n\n function summarizeEvent(obj) {\n const kind = obj?.type || '';\n const message = obj?.message;\n\n if (kind === 'file-history-snapshot') {\n const msgId = obj?.messageId ? `msg=${obj.messageId.slice(0, 8)}...` : null;\n return normalizeSummaryText(msgId || 'file-history-snapshot');\n }\n\n if (kind === 'user' || kind === 'assistant') {\n const role = message?.role || kind;\n const content = message?.content;\n\n if (typeof content === 'string') {\n return normalizeSummaryText(`${role}: ${content}`);\n }\n\n if (Array.isArray(content) && content.length > 0) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n return normalizeSummaryText(`${role}: ${item.text}`);\n }\n if (item?.type === 'thinking' && item.thinking) {\n return normalizeSummaryText(`${role} (thinking): ${item.thinking}`);\n }\n if (item?.type === 'tool_use') {\n const name = item.name || 'tool';\n const input = item.input;\n if (name === 'Bash' && input?.command) {\n return normalizeSummaryText(`${name}: ${input.command}`);\n }\n if (name === 'Read' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Write' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Edit' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Glob' && input?.pattern) {\n return normalizeSummaryText(`${name}: ${input.pattern}`);\n }\n if (name === 'Grep' && input?.pattern) {\n return normalizeSummaryText(`${name}: ${input.pattern}`);\n }\n if (name === 'Task' && input?.description) {\n return normalizeSummaryText(`${name}: ${input.description}`);\n }\n if (input && typeof input === 'object') {\n const keys = Object.keys(input).slice(0, 3);\n return normalizeSummaryText(`${name}(${keys.join(', ')}${Object.keys(input).length > 3 ? ', ...' : ''})`);\n }\n return normalizeSummaryText(`${name}()`);\n }\n if (item?.type === 'tool_result') {\n const toolUseId = item.tool_use_id ? item.tool_use_id.slice(0, 12) : 'result';\n const resultContent = item.content;\n if (typeof resultContent === 'string') {\n return normalizeSummaryText(`tool_result: ${firstLine(resultContent) || toolUseId}`);\n }\n if (Array.isArray(resultContent)) {\n for (const rc of resultContent) {\n if (rc?.type === 'text' && rc.text) {\n return normalizeSummaryText(`tool_result: ${firstLine(rc.text) || toolUseId}`);\n }\n if (rc?.type === 'image') {\n return normalizeSummaryText(`tool_result: [image]`);\n }\n }\n }\n return normalizeSummaryText(`tool_result: ${toolUseId}`);\n }\n if (item?.type === 'image') {\n return normalizeSummaryText(`${role}: [image]`);\n }\n }\n return normalizeSummaryText(`${role}: [${content.length} content items]`);\n }\n\n return kind;\n }\n\n return kind || 'event';\n }\n\n function buildSearchText(obj, summary) {\n const pieces = [];\n pieces.push(obj?.type || '');\n pieces.push(obj?.uuid || '');\n pieces.push(obj?.sessionId || '');\n pieces.push(obj?.cwd || '');\n pieces.push(obj?.gitBranch || '');\n pieces.push(obj?.slug || '');\n const message = obj?.message;\n if (message && typeof message === 'object') {\n pieces.push(message.role || '');\n const msg = extractMessageText(message);\n if (msg) pieces.push(msg);\n const content = message.content;\n if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'tool_use') {\n pieces.push(item.name || '');\n if (item.input && typeof item.input === 'object') {\n pieces.push(JSON.stringify(item.input).slice(0, 500));\n }\n }\n if (item?.type === 'tool_result' && typeof item.content === 'string') {\n pieces.push(item.content.slice(0, 500));\n }\n }\n }\n }\n if (typeof summary === 'string') pieces.push(summary);\n return pieces.join('\\n').toLowerCase();\n }\n\n function stringifyJson(value, truncateLongStrings) {\n const maxLen = 8000;\n return JSON.stringify(value, (key, v) => {\n if (!truncateLongStrings) return v;\n if (typeof v === 'string' && v.length > maxLen) {\n return v.slice(0, maxLen) + `... [truncated ${v.length - maxLen} chars]`;\n }\n return v;\n }, 2);\n }\n\n function updateFilterOptions(events) {\n const typeSet = new Set();\n const contentTypeSet = new Set();\n const roleSet = new Set();\n for (const e of events) {\n if (e.kind) typeSet.add(e.kind);\n for (const ct of e.contentTypes) {\n contentTypeSet.add(ct);\n }\n if (!e.contentTypes.length) contentTypeSet.add('(none)');\n if (e.role) roleSet.add(e.role);\n }\n\n function setOptions(select, items, current) {\n select.innerHTML = '';\n const optAll = document.createElement('option');\n optAll.value = 'all';\n optAll.textContent = 'All';\n select.appendChild(optAll);\n for (const item of items) {\n const opt = document.createElement('option');\n opt.value = item;\n opt.textContent = item;\n select.appendChild(opt);\n }\n select.value = current;\n }\n\n setOptions(els.typeFilter, Array.from(typeSet).sort(), state.filters.type);\n setOptions(els.contentTypeFilter, Array.from(contentTypeSet).sort(), state.filters.contentType);\n setOptions(els.roleFilter, Array.from(roleSet).sort(), state.filters.role);\n\n if (events.length) {\n state.filters.type = els.typeFilter.value || 'all';\n state.filters.contentType = els.contentTypeFilter.value || 'all';\n state.filters.role = els.roleFilter.value || 'all';\n }\n }\n\n function readHashParams() {\n const raw = window.location.hash && window.location.hash.startsWith('#')\n ? window.location.hash.slice(1)\n : '';\n return new URLSearchParams(raw);\n }\n\n function writeHashParams(params) {\n const base = window.location.href.split('#')[0];\n const hash = params.toString();\n const next = base + (hash ? `#${hash}` : '');\n window.history.replaceState(null, '', next);\n }\n\n function syncUrlHashFromControls({ clearSel = false } = {}) {\n if (suppressUrlStateSync) return;\n const params = readHashParams();\n params.set('tz', state.tzMode);\n params.set('q', (els.search.value || '').trim());\n params.set('type', state.filters.type || 'all');\n params.set('ct', state.filters.contentType || 'all');\n params.set('role', state.filters.role || 'all');\n params.set('hide', state.filters.hideThinking ? '1' : '0');\n params.set('truncate', els.truncateStrings.checked ? '1' : '0');\n const selectedLine = state.selected?.line;\n if (typeof selectedLine === 'number' && Number.isFinite(selectedLine)) {\n params.set('sel', String(selectedLine));\n } else if (clearSel) {\n params.delete('sel');\n }\n writeHashParams(params);\n }\n\n function applySelectionFromHash() {\n if (!state.events.length) return;\n const params = readHashParams();\n const raw = params.get('sel');\n if (!raw) return;\n const line = Number(raw);\n if (!Number.isFinite(line)) return;\n const found = state.events.find(e => e.line === line);\n if (!found) return;\n if (state.selectedId === found.id) return;\n suppressUrlStateSync = true;\n try {\n selectEvent(found.id, { scroll: true });\n } finally {\n suppressUrlStateSync = false;\n }\n }\n\n function applyControlsFromHash() {\n const params = readHashParams();\n suppressUrlStateSync = true;\n try {\n const q = params.get('q');\n if (q !== null) {\n els.search.value = q;\n state.filters.query = q.trim().toLowerCase();\n }\n\n const type = params.get('type');\n if (type !== null) state.filters.type = type;\n const ct = params.get('ct');\n if (ct !== null) state.filters.contentType = ct;\n const role = params.get('role');\n if (role !== null) state.filters.role = role;\n\n const hide = params.get('hide');\n if (hide !== null) {\n els.hideThinking.checked = hide === '1' || hide === 'true';\n state.filters.hideThinking = els.hideThinking.checked;\n }\n\n const truncate = params.get('truncate');\n if (truncate !== null) {\n els.truncateStrings.checked = truncate === '1' || truncate === 'true';\n }\n\n const tz = params.get('tz');\n if (tz === 'utc' || tz === 'local') {\n state.tzMode = tz;\n els.tzLocal.classList.toggle('active', tz === 'local');\n els.tzUtc.classList.toggle('active', tz === 'utc');\n }\n } finally {\n suppressUrlStateSync = false;\n }\n }\n\n function applyFilters() {\n const query = state.filters.query;\n const type = state.filters.type;\n const contentType = state.filters.contentType;\n const role = state.filters.role;\n const hideThinking = state.filters.hideThinking;\n\n const out = [];\n for (const e of state.events) {\n if (hideThinking && e.contentTypes.includes('thinking') && e.contentTypes.length === 1) continue;\n if (type !== 'all' && e.kind !== type) continue;\n if (contentType !== 'all') {\n const ct = e.contentTypes.length ? e.contentTypes : ['(none)'];\n if (!ct.includes(contentType)) continue;\n }\n if (role !== 'all' && e.role !== role) continue;\n if (query && !e.search.includes(query)) continue;\n out.push(e);\n }\n\n state.filtered = out;\n els.statsShowing.textContent = String(out.length);\n els.statsTotal.textContent = String(state.events.length);\n\n const start = out.find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n const end = [...out].reverse().find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n if (start && end) {\n els.statsRange.textContent = `${formatTimestamp(start, state.tzMode)} -> ${formatTimestamp(end, state.tzMode)}`;\n } else {\n els.statsRange.textContent = '-';\n }\n\n renderTimeline();\n }\n\n function clearSelection({ syncHash = true } = {}) {\n state.selectedId = null;\n state.selected = null;\n els.detailTitle.textContent = 'Select an event';\n renderDetailImages(null);\n renderPrettyView(null);\n els.detailJson.textContent = state.events.length ? 'Select an event from the timeline.' : 'Load a file to get started.';\n els.detailJson.classList.add('empty');\n els.copyJson.disabled = true;\n els.copyRaw.disabled = true;\n for (const node of els.timeline.querySelectorAll('.event.selected')) {\n node.classList.remove('selected');\n }\n updateDetailPlacement();\n if (syncHash) syncUrlHashFromControls({ clearSel: true });\n }\n\n function renderTimeline() {\n els.timeline.innerHTML = '';\n\n if (!state.filtered.length) {\n const empty = document.createElement('div');\n empty.className = 'empty';\n empty.textContent = state.events.length ? 'No events match your filters.' : 'Load a file to see a timeline.';\n els.timeline.appendChild(empty);\n clearSelection();\n return;\n }\n\n const fragment = document.createDocumentFragment();\n let lastDay = null;\n let prevDate = null;\n\n for (const e of state.filtered) {\n const day = dayKey(e.date, state.tzMode);\n if (day !== lastDay) {\n lastDay = day;\n const sep = document.createElement('div');\n sep.className = 'day-sep';\n sep.textContent = day;\n fragment.appendChild(sep);\n }\n\n const row = document.createElement('div');\n row.className = 'event';\n row.setAttribute('role', 'listitem');\n row.dataset.eventId = String(e.id);\n\n const time = document.createElement('div');\n time.className = 'time';\n\n const ts = document.createElement('div');\n ts.className = 'ts';\n ts.textContent = formatTimestamp(e.date, state.tzMode);\n ts.dataset.kind = 'timestamp';\n\n const meta = document.createElement('div');\n meta.textContent = [\n `#${e.line}`,\n (prevDate && e.date) ? formatDelta(e.date.getTime() - prevDate.getTime()) : null,\n ].filter(Boolean).join(' . ');\n\n time.appendChild(ts);\n time.appendChild(meta);\n\n const body = document.createElement('div');\n\n const badges = document.createElement('div');\n badges.className = 'badges';\n const badgeKind = document.createElement('span');\n badgeKind.className = `badge k-${e.kind}`;\n badgeKind.textContent = e.kind;\n badges.appendChild(badgeKind);\n\n for (const ct of e.contentTypes.slice(0, 2)) {\n const badgeCt = document.createElement('span');\n badgeCt.className = `badge ct-${ct}`;\n badgeCt.textContent = ct;\n badges.appendChild(badgeCt);\n }\n if (e.contentTypes.length > 2) {\n const badgeMore = document.createElement('span');\n badgeMore.className = 'badge';\n badgeMore.textContent = `+${e.contentTypes.length - 2}`;\n badges.appendChild(badgeMore);\n }\n\n if (e.role) {\n const badgeRole = document.createElement('span');\n badgeRole.className = `badge role-${e.role}`;\n badgeRole.textContent = e.role;\n badges.appendChild(badgeRole);\n }\n\n const summary = document.createElement('div');\n summary.className = 'summary';\n summary.textContent = e.summary;\n\n body.appendChild(badges);\n body.appendChild(summary);\n\n row.appendChild(time);\n row.appendChild(body);\n\n row.addEventListener('click', () => selectEvent(e.id, { focusTimeline: true }));\n fragment.appendChild(row);\n\n if (e.date instanceof Date && !Number.isNaN(e.date.getTime())) prevDate = e.date;\n }\n\n els.timeline.appendChild(fragment);\n\n if (state.selectedId !== null) {\n const stillThere = state.filtered.some(e => e.id === state.selectedId);\n if (!stillThere) clearSelection();\n else selectEvent(state.selectedId, { scroll: false });\n } else {\n clearSelection({ syncHash: false });\n }\n }\n\n function selectEvent(id, { scroll = false, focusTimeline = false } = {}) {\n const e = state.filtered.find(item => item.id === id) || state.events.find(item => item.id === id);\n if (!e) return;\n state.selectedId = id;\n state.selected = e;\n\n if (focusTimeline) {\n els.timeline.focus({ preventScroll: true });\n }\n\n for (const node of els.timeline.querySelectorAll('.event.selected')) {\n node.classList.remove('selected');\n }\n const node = els.timeline.querySelector(`.event[data-event-id=\"${CSS.escape(String(id))}\"]`);\n if (node) {\n node.classList.add('selected');\n updateDetailPlacement();\n if (scroll) node.scrollIntoView({ block: 'center' });\n }\n\n els.detailJson.classList.remove('empty');\n const titleParts = [\n formatTimestamp(e.date, state.tzMode),\n `#${e.line}`,\n e.kind,\n e.role ? `(${e.role})` : null,\n ].filter(Boolean);\n els.detailTitle.textContent = titleParts.join(' ');\n\n const shouldTruncate = els.truncateStrings.checked;\n renderDetailImages(e.obj);\n renderPrettyView(e.obj);\n els.detailJson.textContent = stringifyJson(e.obj, shouldTruncate) || '';\n els.copyJson.disabled = false;\n els.copyRaw.disabled = false;\n updateDetailPlacement();\n syncUrlHashFromControls();\n }\n\n function extractImageUrls(obj) {\n const urls = [];\n const message = obj?.message;\n if (!message || typeof message !== 'object') return urls;\n const content = message.content;\n if (!Array.isArray(content)) return urls;\n\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n\n // Direct image in content\n if (item.type === 'image' && item.source?.type === 'base64' && item.source?.data) {\n const mediaType = item.source.media_type || 'image/png';\n urls.push(`data:${mediaType};base64,${item.source.data}`);\n }\n\n // Image in tool_result content\n if (item.type === 'tool_result' && Array.isArray(item.content)) {\n for (const rc of item.content) {\n if (rc?.type === 'image' && rc.source?.type === 'base64' && rc.source?.data) {\n const mediaType = rc.source.media_type || 'image/png';\n urls.push(`data:${mediaType};base64,${rc.source.data}`);\n }\n }\n }\n }\n\n return urls;\n }\n\n function escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n // Initialize markdown-it with GFM-like settings\n const md = window.markdownit({\n html: false, // Disable raw HTML input\n linkify: true, // Auto-convert URLs to links\n typographer: true, // Smart quotes, dashes\n breaks: true // Convert \\n to <br>\n });\n\n function renderMarkdown(text) {\n if (typeof text !== 'string') return '';\n\n // Render markdown to HTML\n const html = md.render(text);\n\n // Sanitize with DOMPurify to prevent XSS\n return DOMPurify.sanitize(html);\n }\n\n function renderPrettyView(obj) {\n els.detailPretty.innerHTML = '';\n\n if (!obj || typeof obj !== 'object') {\n els.detailPretty.hidden = true;\n return;\n }\n\n const message = obj.message;\n if (!message || typeof message !== 'object') {\n els.detailPretty.hidden = true;\n return;\n }\n\n const content = message.content;\n const fragment = document.createDocumentFragment();\n let hasContent = false;\n\n // Handle string content (simple text message)\n if (typeof content === 'string') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = message.role || 'Message';\n const body = document.createElement('div');\n body.className = 'pretty-content';\n body.innerHTML = renderMarkdown(content);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Handle array content\n if (Array.isArray(content)) {\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n\n // Text content\n if (item.type === 'text' && item.text) {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = message.role === 'assistant' ? 'Response' : 'Message';\n const body = document.createElement('div');\n body.className = 'pretty-content';\n body.innerHTML = renderMarkdown(item.text);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Thinking content\n if (item.type === 'thinking' && item.thinking) {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = 'Thinking';\n const body = document.createElement('div');\n body.className = 'thinking-block pretty-content';\n body.innerHTML = renderMarkdown(item.thinking);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Tool use\n if (item.type === 'tool_use') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = 'Tool Call';\n\n const toolName = item.name || 'Unknown Tool';\n const input = item.input || {};\n\n // Special handling for Bash command\n if (toolName === 'Bash' && input.command) {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n if (input.description) {\n const descSpan = document.createElement('span');\n descSpan.className = 'tool-desc';\n descSpan.textContent = input.description;\n header.appendChild(descSpan);\n }\n\n const cmdInput = document.createElement('div');\n cmdInput.className = 'command-input';\n cmdInput.textContent = input.command;\n\n block.appendChild(header);\n block.appendChild(cmdInput);\n section.appendChild(label);\n section.appendChild(block);\n }\n // Special handling for file operations\n else if (['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(toolName)) {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n\n block.appendChild(header);\n\n const params = document.createElement('div');\n params.className = 'tool-params';\n params.style.padding = '10px 12px';\n\n for (const [key, value] of Object.entries(input)) {\n const param = document.createElement('div');\n param.className = 'tool-param';\n const paramName = document.createElement('span');\n paramName.className = 'tool-param-name';\n paramName.textContent = key;\n const paramValue = document.createElement('span');\n paramValue.className = 'tool-param-value';\n if (typeof value === 'string') {\n paramValue.textContent = value.length > 500 ? value.slice(0, 500) + '...' : value;\n } else {\n paramValue.textContent = JSON.stringify(value, null, 2);\n }\n param.appendChild(paramName);\n param.appendChild(paramValue);\n params.appendChild(param);\n }\n\n block.appendChild(params);\n section.appendChild(label);\n section.appendChild(block);\n }\n // Generic tool display\n else {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n\n block.appendChild(header);\n\n if (Object.keys(input).length > 0) {\n const params = document.createElement('div');\n params.className = 'tool-params';\n params.style.padding = '10px 12px';\n\n for (const [key, value] of Object.entries(input)) {\n const param = document.createElement('div');\n param.className = 'tool-param';\n const paramName = document.createElement('span');\n paramName.className = 'tool-param-name';\n paramName.textContent = key;\n const paramValue = document.createElement('span');\n paramValue.className = 'tool-param-value';\n if (typeof value === 'string') {\n paramValue.textContent = value.length > 1000 ? value.slice(0, 1000) + '...' : value;\n } else {\n paramValue.textContent = JSON.stringify(value, null, 2);\n }\n param.appendChild(paramName);\n param.appendChild(paramValue);\n params.appendChild(param);\n }\n\n block.appendChild(params);\n }\n\n section.appendChild(label);\n section.appendChild(block);\n }\n\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Tool result\n if (item.type === 'tool_result') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = item.is_error ? 'Tool Error' : 'Tool Result';\n\n const resultContent = item.content;\n let resultText = '';\n\n if (typeof resultContent === 'string') {\n resultText = resultContent;\n } else if (Array.isArray(resultContent)) {\n for (const rc of resultContent) {\n if (rc?.type === 'text' && rc.text) {\n resultText += rc.text;\n }\n }\n }\n\n if (resultText) {\n const block = document.createElement('div');\n block.className = 'pretty-content' + (item.is_error ? ' result-error' : '');\n const displayText = resultText.length > 5000 ? resultText.slice(0, 5000) + '\\n... [truncated]' : resultText;\n block.innerHTML = renderMarkdown(displayText);\n section.appendChild(label);\n section.appendChild(block);\n fragment.appendChild(section);\n hasContent = true;\n }\n }\n }\n }\n\n if (hasContent) {\n els.detailPretty.appendChild(fragment);\n els.detailPretty.hidden = false;\n } else {\n els.detailPretty.hidden = true;\n }\n }\n\n function openImageModal(src, altText = '') {\n if (!src) return;\n lastActiveElementBeforeModal = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n lastBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n\n els.imageModalImg.src = src;\n els.imageModalImg.alt = altText;\n els.imageModal.classList.add('open');\n els.imageModal.setAttribute('aria-hidden', 'false');\n els.imageModalClose.focus();\n }\n\n function closeImageModal() {\n if (!els.imageModal.classList.contains('open')) return;\n els.imageModal.classList.remove('open');\n els.imageModal.setAttribute('aria-hidden', 'true');\n els.imageModalImg.removeAttribute('src');\n els.imageModalImg.alt = '';\n document.body.style.overflow = lastBodyOverflow ?? '';\n\n if (lastActiveElementBeforeModal) {\n lastActiveElementBeforeModal.focus();\n }\n lastActiveElementBeforeModal = null;\n lastBodyOverflow = null;\n }\n\n function renderDetailImages(obj) {\n const urls = extractImageUrls(obj);\n els.detailImages.innerHTML = '';\n if (!urls.length) {\n els.detailImages.hidden = true;\n return;\n }\n const fragment = document.createDocumentFragment();\n urls.forEach((src, idx) => {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'thumb';\n btn.title = `Open image ${idx + 1}`;\n const img = document.createElement('img');\n img.loading = 'lazy';\n img.src = src;\n img.alt = `Image ${idx + 1}`;\n btn.appendChild(img);\n btn.addEventListener('click', () => openImageModal(src, `Image ${idx + 1}`));\n fragment.appendChild(btn);\n });\n els.detailImages.appendChild(fragment);\n els.detailImages.hidden = false;\n }\n\n function isEditableElement(el) {\n if (!(el instanceof HTMLElement)) return false;\n if (el.isContentEditable) return true;\n const tag = el.tagName;\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';\n }\n\n function moveSelection(delta) {\n if (!state.filtered.length) return;\n const idx = state.filtered.findIndex(e => e.id === state.selectedId);\n if (idx === -1) {\n selectEvent(state.filtered[0].id, { scroll: true });\n return;\n }\n const nextIdx = Math.max(0, Math.min(state.filtered.length - 1, idx + delta));\n if (nextIdx === idx) return;\n selectEvent(state.filtered[nextIdx].id, { scroll: true });\n }\n\n async function copyText(text) {\n if (typeof text !== 'string') return;\n try {\n await navigator.clipboard.writeText(text);\n setStatus('Copied to clipboard.', 'good');\n } catch (error) {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n document.body.removeChild(ta);\n setStatus('Copied to clipboard.', 'good');\n }\n }\n\n function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n\n const message = e.obj?.message;\n if (!message) continue;\n\n const content = message.content;\n if (typeof content === 'string') {\n prompts.push(content);\n } else if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n prompts.push(item.text);\n }\n }\n }\n }\n return prompts.join('\\n\\n');\n }\n\n function openPromptsModal(text) {\n lastActiveElementBeforePrompts = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n lastBodyOverflowBeforePrompts = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n\n els.promptsText.value = text || '';\n els.promptsModal.classList.add('open');\n els.promptsModal.setAttribute('aria-hidden', 'false');\n els.promptsClose.focus();\n els.promptsText.scrollTop = 0;\n }\n\n function closePromptsModal() {\n if (!els.promptsModal.classList.contains('open')) return;\n els.promptsModal.classList.remove('open');\n els.promptsModal.setAttribute('aria-hidden', 'true');\n els.promptsText.value = '';\n document.body.style.overflow = lastBodyOverflowBeforePrompts ?? '';\n\n if (lastActiveElementBeforePrompts) lastActiveElementBeforePrompts.focus();\n lastActiveElementBeforePrompts = null;\n lastBodyOverflowBeforePrompts = null;\n }\n\n function setTimezoneMode(mode) {\n state.tzMode = mode;\n els.tzLocal.classList.toggle('active', mode === 'local');\n els.tzUtc.classList.toggle('active', mode === 'utc');\n applyFilters();\n if (state.selectedId !== null) selectEvent(state.selectedId);\n syncUrlHashFromControls();\n }\n\n els.promptsClose.addEventListener('click', closePromptsModal);\n els.promptsModal.addEventListener('click', (event) => {\n if (event.target === els.promptsModal) closePromptsModal();\n });\n els.promptsCopy.addEventListener('click', async () => {\n const text = els.promptsText.value || '';\n if (!text.trim()) {\n setStatus('No user prompts found.', 'warn');\n return;\n }\n await copyText(text);\n if (promptsCopiedTimer) clearTimeout(promptsCopiedTimer);\n const originalText = els.promptsCopy.textContent;\n els.promptsCopy.textContent = 'Copied!';\n promptsCopiedTimer = setTimeout(() => {\n els.promptsCopy.textContent = originalText;\n promptsCopiedTimer = null;\n }, 1500);\n });\n els.showPrompts.addEventListener('click', () => {\n const text = extractUserPrompts();\n if (!text) {\n setStatus('No user prompts found.', 'warn');\n return;\n }\n openPromptsModal(text);\n });\n\n els.imageModalClose.addEventListener('click', closeImageModal);\n els.imageModal.addEventListener('click', (event) => {\n if (event.target === els.imageModal) closeImageModal();\n });\n document.addEventListener('keydown', (event) => {\n if (event.key !== 'Escape') return;\n if (els.promptsModal.classList.contains('open')) closePromptsModal();\n if (els.imageModal.classList.contains('open')) closeImageModal();\n });\n\n document.addEventListener('keydown', (event) => {\n if (els.imageModal.classList.contains('open')) return;\n if (els.promptsModal.classList.contains('open')) return;\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;\n if (isEditableElement(document.activeElement)) return;\n if (document.activeElement !== els.timeline && !els.timeline.contains(document.activeElement)) return;\n if (state.selectedId === null) return;\n event.preventDefault();\n moveSelection(event.key === 'ArrowUp' ? -1 : 1);\n });\n\n if (typeof wideLayoutMq.addEventListener === 'function') {\n wideLayoutMq.addEventListener('change', updateDetailPlacement);\n } else if (typeof wideLayoutMq.addListener === 'function') {\n wideLayoutMq.addListener(updateDetailPlacement);\n }\n\n function setTab(tabName) {\n for (const tab of document.querySelectorAll('.tab')) {\n const active = tab.dataset.tab === tabName;\n tab.classList.toggle('active', active);\n tab.setAttribute('aria-selected', active ? 'true' : 'false');\n }\n for (const panel of document.querySelectorAll('[data-panel]')) {\n panel.hidden = panel.dataset.panel !== tabName;\n }\n }\n\n function setShareUrlParam(url) {\n const current = new URL(window.location.href);\n if (url) current.searchParams.set('url', url);\n else current.searchParams.delete('url');\n window.history.replaceState(null, '', current);\n }\n\n async function fetchAndRenderUrl(url, { syncUrl = true } = {}) {\n const trimmed = (url || '').trim();\n if (!trimmed) {\n setStatus('Enter a URL first.', 'error');\n return;\n }\n if (syncUrl) setShareUrlParam(trimmed);\n setStatus(`Fetching ${trimmed}...`, 'warn');\n try {\n const resp = await fetch(trimmed, { mode: 'cors' });\n if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n const text = await resp.text();\n await loadText(text, trimmed);\n } catch (error) {\n setStatus(`Failed to fetch URL: ${String(error)}`, 'error');\n }\n }\n\n async function loadText(text, sourceLabel) {\n const started = performance.now();\n state.sourceLabel = sourceLabel || null;\n setStatus(`Parsing ${sourceLabel || 'content'}...`, 'warn');\n closePromptsModal();\n els.showPrompts.disabled = true;\n clearSelection({ syncHash: false });\n\n const lines = text.split(/\\r?\\n/);\n const events = [];\n const errors = [];\n let idCounter = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (!line.trim()) continue;\n const parsed = safeJsonParse(line);\n if (!parsed.ok) {\n errors.push({ line: i + 1, error: parsed.error });\n continue;\n }\n const obj = parsed.value;\n const ts = typeof obj.timestamp === 'string' ? new Date(obj.timestamp) : null;\n const kind = obj.type || '(unknown)';\n const message = obj.message && typeof obj.message === 'object' ? obj.message : null;\n const contentTypes = message ? getContentTypes(message) : [];\n const role = message?.role ?? null;\n\n const summary = summarizeEvent(obj);\n events.push({\n id: ++idCounter,\n line: i + 1,\n raw: line,\n obj,\n date: ts,\n kind,\n contentTypes,\n role,\n summary,\n search: buildSearchText(obj, summary),\n });\n }\n\n state.events = events;\n els.showPrompts.disabled = !events.length;\n updateFilterOptions(events);\n applyFilters();\n applySelectionFromHash();\n syncUrlHashFromControls();\n\n const durationMs = Math.round(performance.now() - started);\n if (errors.length) {\n const first = errors[0];\n setStatus(\n `Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.\\n` +\n `Could not parse ${errors.length} line(s). First error at line ${first.line}: ${String(first.error)}`,\n 'warn'\n );\n } else {\n setStatus(`Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.`, 'good');\n }\n }\n\n async function handleFile(file) {\n if (!file) return;\n setShareUrlParam(null);\n try {\n const text = await file.text();\n await loadText(text, file.name);\n } catch (error) {\n setStatus(`Failed to read file: ${String(error)}`, 'error');\n }\n }\n\n els.dropzone.addEventListener('click', () => els.fileInput.click());\n els.dropzone.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n els.fileInput.click();\n }\n });\n els.fileInput.addEventListener('change', () => handleFile(els.fileInput.files?.[0]));\n\n els.dropzone.addEventListener('dragover', (e) => {\n e.preventDefault();\n els.dropzone.classList.add('dragover');\n });\n els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('dragover'));\n els.dropzone.addEventListener('drop', (e) => {\n e.preventDefault();\n els.dropzone.classList.remove('dragover');\n const file = e.dataTransfer?.files?.[0];\n if (file) handleFile(file);\n });\n\n els.renderPaste.addEventListener('click', async () => {\n const text = els.pasteInput.value || '';\n if (!text.trim()) {\n setStatus('Paste some JSONL content first.', 'error');\n return;\n }\n setShareUrlParam(null);\n await loadText(text, 'pasted content');\n });\n\n els.fetchUrl.addEventListener('click', async () => {\n await fetchAndRenderUrl(els.urlInput.value, { syncUrl: true });\n });\n\n for (const tab of document.querySelectorAll('.tab')) {\n tab.addEventListener('click', () => setTab(tab.dataset.tab));\n }\n\n els.tzLocal.addEventListener('click', () => setTimezoneMode('local'));\n els.tzUtc.addEventListener('click', () => setTimezoneMode('utc'));\n\n els.search.addEventListener('input', () => {\n state.filters.query = els.search.value.trim().toLowerCase();\n applyFilters();\n syncUrlHashFromControls();\n });\n els.typeFilter.addEventListener('change', () => {\n state.filters.type = els.typeFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.contentTypeFilter.addEventListener('change', () => {\n state.filters.contentType = els.contentTypeFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.roleFilter.addEventListener('change', () => {\n state.filters.role = els.roleFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.hideThinking.addEventListener('change', () => {\n state.filters.hideThinking = els.hideThinking.checked;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.truncateStrings.addEventListener('change', () => {\n if (state.selectedId !== null) selectEvent(state.selectedId);\n syncUrlHashFromControls();\n });\n\n els.clearFilters.addEventListener('click', () => {\n els.search.value = '';\n state.filters.query = '';\n state.filters.type = 'all';\n state.filters.contentType = 'all';\n state.filters.role = 'all';\n state.filters.hideThinking = false;\n els.hideThinking.checked = false;\n updateFilterOptions(state.events);\n applyFilters();\n syncUrlHashFromControls();\n });\n\n els.copyJson.addEventListener('click', () => {\n if (!state.selected) return;\n copyText(stringifyJson(state.selected.obj, false));\n });\n els.copyRaw.addEventListener('click', () => {\n if (!state.selected) return;\n copyText(state.selected.raw);\n });\n\n async function initFromQuery() {\n const url = new URLSearchParams(window.location.search).get('url');\n if (!url) return;\n setTab('url');\n els.urlInput.value = url;\n await fetchAndRenderUrl(url, { syncUrl: false });\n }\n\n // Initialize select options for the empty state.\n applyControlsFromHash();\n updateFilterOptions([]);\n syncUrlHashFromControls();\n window.addEventListener('hashchange', () => {\n applyControlsFromHash();\n updateFilterOptions(state.events);\n applyFilters();\n applySelectionFromHash();\n });\n void initFromQuery();\n </script>\n</body>\n</html>\n","structuredPatch":[{"oldStart":2103,"oldLines":6,"newStart":2103,"newLines":8,"lines":[" if (e.kind !== 'user') continue;"," if (e.role !== 'user') continue;"," if (!e.contentTypes.includes('text')) continue;","+ // Skip compact summary messages (auto-generated context summaries)","+ if (e.obj?.isCompactSummary === true) continue;"," "," const message = e.obj?.message;"," if (!message) continue;"]}],"userModified":false,"replaceAll":false}}
{"parentUuid":"c0c69e20-7bb6-4612-8685-f973e74d16e7","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01DTSd8c5SuyKQntKY7DLGtX","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The edit was successful. I added a check to skip any events where `e.obj?.isCompactSummary === true`. This will filter out compact summary messages when gathering user prompts for the \"Show prompts\" feature.","signature":"EvgCCkYIChgCKkDSYDhnSKwdMSNp7RkF3bFIce6n7Xp2cEP2szPhHijSG3WbIF4S/6a3wR61X5Cje77k/QIiaJ39ZwCwpQ8sQWZbEgz8CygH3zPzJAWgOL4aDDiHqts4wcAquxiILSIwAuneHyuwfT3w+v6ePXj3WyfIRK+Jbpt72uur1lDDFjVmB4VImwjfXXD/VeL2CkpqKt8BaXmo8cwNbK4h9N0OfCu2LqgErR+vyEvj4XcfKFHhP1C8Kn8sPi4Buj7rQuXCcMkckLiNX2hadLkUF0jB/C15PxCjIXEinKlkP2BgDSsxCDLCIpECTcaNV2gRsdgM1aaECb/OcGQF4fiZqBemAYbaEDdbAXBsfUECffXTNcKzwwi23etJ2Lz2Ywr9VXoqOjK4yifju8TAhxprT4qNXl/nanPMFs7fIFNrn2pbXD4qhMKPb56QRiDZPfGMUfP3GZsSpYk2HZ82lTJQJWBEuf5Tb6iMuqbN5bB1V9771qLBdRgB"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":1221,"cache_read_input_tokens":53143,"cache_creation":{"ephemeral_5m_input_tokens":1221,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CWFQrWHNSkDexwpcvmVih","type":"assistant","uuid":"6c1a4919-eb94-4d3f-b3ec-bc0745484465","timestamp":"2025-12-19T04:26:27.766Z"}
{"parentUuid":"6c1a4919-eb94-4d3f-b3ec-bc0745484465","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01DTSd8c5SuyKQntKY7DLGtX","type":"message","role":"assistant","content":[{"type":"text","text":"Done. The `extractUserPrompts` function now skips any prompts where `isCompactSummary` is `true` in the JSON object. When you click \"Show prompts\", it will no longer include auto-generated context summaries."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":1221,"cache_read_input_tokens":53143,"cache_creation":{"ephemeral_5m_input_tokens":1221,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CWFQrWHNSkDexwpcvmVih","type":"assistant","uuid":"5b85dbfa-4f82-42ec-980a-36225c0ed91d","timestamp":"2025-12-19T04:26:28.691Z"}
{"type":"file-history-snapshot","messageId":"592fd7f7-2428-4adb-af1a-d425ec6c00a0","snapshot":{"messageId":"592fd7f7-2428-4adb-af1a-d425ec6c00a0","trackedFileBackups":{"claude-code-timeline.html":{"backupFileName":"cc3a5887c9685f80@v1","version":1,"backupTime":"2025-12-19T04:26:24.003Z"}},"timestamp":"2025-12-19T04:28:33.299Z"},"isSnapshotUpdate":false}
{"parentUuid":"5b85dbfa-4f82-42ec-980a-36225c0ed91d","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":"Also skip \"isMeta\": true"},"uuid":"592fd7f7-2428-4adb-af1a-d425ec6c00a0","timestamp":"2025-12-19T04:28:33.293Z","thinkingMetadata":{"level":"high","disabled":false,"triggers":[]},"todos":[]}
{"parentUuid":"592fd7f7-2428-4adb-af1a-d425ec6c00a0","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01Lh2gVrkoNiqAKaJx7wcxya","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants to also skip prompts that have \"isMeta\": true in their JSON object. I'll add another condition to the extractUserPrompts function.","signature":"EroCCkYIChgCKkDDDv4ye2itHslq5CbHWRpKU4uHjAdVU/sie12jA/c1r6BGMtFZgXamXVpfygaMZsY3UPFvlTB7ZamLBbOIy4bVEgz5IMJKLWLhm0ZlhtQaDD2lpDryrxNL5B47FCIwL1hKuf0gS2T1ocQhhoDNVQfhV/6gRRWuTWBeMOlVz+MKC2Sh/IC0Ive/lZyzKvNhKqEBAJHX9DYLS8B6IEZRYuD4rqkzCTb4ij4WVIcU2AsD+WjsromLL9NPLwNbKwt2bBCfbxsVoMs/EGyx1hh1OMzTtMR2nJzSMlZTN2JhMb6D7Obi5IdOvDvLwGOF0nBe/+XVDDiE53iLIWC7o8SVZXMcNosZRHGiFccYxZPkH1gZKNK5sqRY5VYsV/9jI73Nd/QsWOTP4DQCXLi94HhbPOMoC10YAQ=="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":11,"cache_read_input_tokens":54483,"cache_creation":{"ephemeral_5m_input_tokens":11,"ephemeral_1h_input_tokens":0},"output_tokens":4,"service_tier":"standard"}},"requestId":"req_011CWFR23d2WxKBU23ZbfNE7","type":"assistant","uuid":"722618e8-0a5c-405b-ad02-49c4db1c3bec","timestamp":"2025-12-19T04:28:36.399Z"}
{"parentUuid":"722618e8-0a5c-405b-ad02-49c4db1c3bec","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01Lh2gVrkoNiqAKaJx7wcxya","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BvWbg2BkMKhQ8npkcZHfmX","name":"Edit","input":{"file_path":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html","old_string":" // Skip compact summary messages (auto-generated context summaries)\n if (e.obj?.isCompactSummary === true) continue;","new_string":" // Skip compact summary messages (auto-generated context summaries) and meta messages\n if (e.obj?.isCompactSummary === true) continue;\n if (e.obj?.isMeta === true) continue;"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":11,"cache_read_input_tokens":54483,"cache_creation":{"ephemeral_5m_input_tokens":11,"ephemeral_1h_input_tokens":0},"output_tokens":233,"service_tier":"standard"}},"requestId":"req_011CWFR23d2WxKBU23ZbfNE7","type":"assistant","uuid":"c7c77b5d-986d-4f80-ad4d-4b54eed92fb6","timestamp":"2025-12-19T04:28:37.710Z"}
{"parentUuid":"c7c77b5d-986d-4f80-ad4d-4b54eed92fb6","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BvWbg2BkMKhQ8npkcZHfmX","type":"tool_result","content":"The file /Users/simon/Dropbox/dev/tools/claude-code-timeline.html has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n 2102→ // Filter: type=user, content-type includes text, role=user\n 2103→ if (e.kind !== 'user') continue;\n 2104→ if (e.role !== 'user') continue;\n 2105→ if (!e.contentTypes.includes('text')) continue;\n 2106→ // Skip compact summary messages (auto-generated context summaries) and meta messages\n 2107→ if (e.obj?.isCompactSummary === true) continue;\n 2108→ if (e.obj?.isMeta === true) continue;\n 2109→\n 2110→ const message = e.obj?.message;\n 2111→ if (!message) continue;\n 2112→"}]},"uuid":"e29b0c9d-f838-4338-8c38-0d764b850dd5","timestamp":"2025-12-19T04:28:37.746Z","toolUseResult":{"filePath":"/Users/simon/Dropbox/dev/tools/claude-code-timeline.html","oldString":" // Skip compact summary messages (auto-generated context summaries)\n if (e.obj?.isCompactSummary === true) continue;","newString":" // Skip compact summary messages (auto-generated context summaries) and meta messages\n if (e.obj?.isCompactSummary === true) continue;\n if (e.obj?.isMeta === true) continue;","originalFile":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Claude Code Timeline Viewer</title>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.3/purify.min.js\" integrity=\"sha384-osZDKVu4ipZP703HmPOhWdyBajcFyjX2Psjk//TG1Rc0AdwEtuToaylrmcK3LdAl\" crossorigin=\"anonymous\"></script>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/markdown-it/4.4.0/markdown-it.min.js\" integrity=\"sha384-cxGp+QDeMxACP73aA0Lj0Sc/h4z1X73+umcvCyI0Vbyh3OYxgrPI+0mxG9slsMTN\" crossorigin=\"anonymous\"></script>\n <style>\n :root {\n color-scheme: light dark;\n --bg: #0b0c0f;\n --panel: #12141a;\n --panel-2: #0f1116;\n --text: #e8eaf0;\n --muted: #a6adbf;\n --line: rgba(255, 255, 255, 0.10);\n --line-2: rgba(255, 255, 255, 0.16);\n --accent: #6aa9ff;\n --accent-2: #8a5cff;\n --good: #46d39a;\n --warn: #ffcc66;\n --bad: #ff6b6b;\n --shadow: 0 18px 50px rgba(0, 0, 0, 0.35);\n --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,\n \"Apple Color Emoji\", \"Segoe UI Emoji\";\n }\n\n @media (prefers-color-scheme: light) {\n :root {\n --bg: #f7f8fc;\n --panel: #ffffff;\n --panel-2: #ffffff;\n --text: #10121a;\n --muted: #5b647a;\n --line: rgba(16, 18, 26, 0.12);\n --line-2: rgba(16, 18, 26, 0.18);\n --shadow: 0 18px 50px rgba(16, 18, 26, 0.10);\n }\n }\n\n * { box-sizing: border-box; }\n html, body { height: 100%; }\n body {\n margin: 0;\n font-family: var(--sans);\n background: radial-gradient(1200px 600px at 10% -10%, rgba(106, 169, 255, 0.18), transparent 45%),\n radial-gradient(1200px 600px at 90% -20%, rgba(138, 92, 255, 0.16), transparent 50%),\n var(--bg);\n color: var(--text);\n }\n\n .wrap {\n max-width: none;\n margin: 0;\n padding: 22px 16px 42px;\n }\n\n header {\n display: grid;\n gap: 10px;\n margin-bottom: 14px;\n }\n\n h1 {\n margin: 0;\n font-size: clamp(22px, 3vw, 32px);\n letter-spacing: 0.2px;\n }\n\n .sub {\n margin: 6px 0 0;\n color: var(--muted);\n line-height: 1.35;\n max-width: 70ch;\n }\n\n .controls {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n align-items: center;\n justify-content: flex-start;\n }\n\n .controls .field {\n display: grid;\n gap: 4px;\n min-width: 180px;\n }\n\n .label {\n font-size: 12px;\n color: var(--muted);\n }\n\n input[type=\"search\"], input[type=\"url\"], select, textarea {\n font: inherit;\n color: inherit;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n border-radius: 10px;\n padding: 10px 11px;\n outline: none;\n }\n\n input[type=\"search\"]:focus, input[type=\"url\"]:focus, select:focus, textarea:focus {\n border-color: color-mix(in srgb, var(--accent) 60%, var(--line));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .segmented {\n display: inline-flex;\n border: 1px solid var(--line);\n border-radius: 999px;\n overflow: hidden;\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n }\n\n .segmented button {\n border: none;\n padding: 9px 12px;\n font: inherit;\n background: transparent;\n color: var(--muted);\n cursor: pointer;\n min-width: 74px;\n }\n\n .segmented button.active {\n background: color-mix(in srgb, var(--accent) 16%, transparent);\n color: var(--text);\n }\n\n .segmented button:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);\n outline-offset: -2px;\n }\n\n .btn {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n color: var(--text);\n border-radius: 10px;\n padding: 10px 12px;\n font: inherit;\n cursor: pointer;\n transition: transform 80ms ease, border-color 120ms ease;\n }\n\n .btn:hover { border-color: var(--line-2); }\n .btn:active { transform: translateY(1px); }\n .btn.primary {\n border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n background: color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .btn:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n }\n\n .panel {\n background: var(--panel);\n border: 1px solid var(--line);\n border-radius: 16px;\n box-shadow: var(--shadow);\n }\n\n .input-panel {\n padding: 14px;\n margin-bottom: 14px;\n }\n\n .filters-panel {\n padding: 14px;\n margin-bottom: 14px;\n }\n\n .input-grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 14px;\n align-items: stretch;\n }\n\n @media (min-width: 920px) {\n .input-grid {\n grid-template-columns: 380px 1fr;\n }\n }\n\n .dropzone {\n border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--line));\n border-radius: 14px;\n background: linear-gradient(\n 135deg,\n color-mix(in srgb, var(--accent) 12%, transparent),\n transparent 45%,\n color-mix(in srgb, var(--accent-2) 10%, transparent)\n );\n padding: 16px;\n display: grid;\n gap: 6px;\n cursor: pointer;\n min-height: 124px;\n align-content: center;\n }\n\n .dropzone.dragover {\n border-style: solid;\n border-color: color-mix(in srgb, var(--accent) 70%, var(--line));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);\n }\n\n .dropzone strong { font-size: 15px; }\n .muted { color: var(--muted); }\n .tiny { font-size: 12px; color: var(--muted); }\n\n .tabs {\n display: flex;\n gap: 8px;\n margin-bottom: 10px;\n flex-wrap: wrap;\n align-items: center;\n }\n\n .tab {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n color: var(--muted);\n border-radius: 999px;\n padding: 7px 10px;\n cursor: pointer;\n font: inherit;\n }\n\n .tab.active {\n color: var(--text);\n border-color: color-mix(in srgb, var(--accent) 55%, var(--line));\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n }\n\n textarea {\n width: 100%;\n min-height: 190px;\n resize: vertical;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n }\n\n .row {\n display: flex;\n gap: 10px;\n flex-wrap: wrap;\n align-items: center;\n margin-top: 10px;\n }\n\n .status {\n margin-top: 12px;\n padding: 10px 12px;\n border-radius: 12px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 94%, transparent);\n display: none;\n white-space: pre-wrap;\n word-break: break-word;\n }\n .status.visible { display: block; }\n .status.error { border-color: color-mix(in srgb, var(--bad) 60%, var(--line)); }\n .status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n .status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n\n main {\n display: grid;\n gap: 14px;\n grid-template-columns: 1fr;\n min-height: 560px;\n }\n\n @media (min-width: 1060px) {\n main {\n grid-template-columns: 1fr 520px;\n align-items: stretch;\n }\n }\n\n .timeline-panel {\n display: grid;\n grid-template-rows: auto 1fr;\n min-height: 560px;\n }\n\n .stats {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n }\n\n .stats .big {\n font-size: 14px;\n font-weight: 600;\n }\n\n .timeline {\n overflow: auto;\n padding: 8px 0;\n }\n\n .timeline:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);\n outline-offset: -2px;\n border-radius: 14px;\n }\n\n .day-sep {\n position: sticky;\n top: 0;\n z-index: 2;\n padding: 8px 14px;\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n border-top: 1px solid var(--line);\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n color: var(--muted);\n backdrop-filter: blur(6px);\n }\n\n .event {\n display: grid;\n grid-template-columns: 176px 1fr;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid color-mix(in srgb, var(--line) 85%, transparent);\n cursor: pointer;\n }\n\n .event:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }\n .event.selected {\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);\n outline-offset: -2px;\n }\n\n .time {\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.35;\n color: var(--muted);\n display: grid;\n gap: 3px;\n }\n\n .time .ts {\n color: var(--text);\n font-weight: 600;\n }\n\n .badges {\n display: flex;\n gap: 6px;\n flex-wrap: wrap;\n align-items: center;\n margin-bottom: 6px;\n }\n\n .badge {\n font-size: 11px;\n font-family: var(--mono);\n padding: 2px 7px;\n border-radius: 999px;\n border: 1px solid var(--line);\n color: var(--muted);\n background: color-mix(in srgb, var(--panel-2) 90%, transparent);\n }\n\n .badge.k-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n .badge.k-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n .badge.k-file-history-snapshot { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); color: color-mix(in srgb, var(--warn) 75%, var(--text)); }\n\n .badge.ct-text { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); color: color-mix(in srgb, var(--good) 75%, var(--text)); }\n .badge.ct-thinking { border-color: color-mix(in srgb, var(--accent-2) 55%, var(--line)); color: color-mix(in srgb, var(--accent-2) 75%, var(--text)); }\n .badge.ct-tool_use { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); color: color-mix(in srgb, var(--accent) 75%, var(--text)); }\n .badge.ct-tool_result { border-color: color-mix(in srgb, #8690a8 55%, var(--line)); color: color-mix(in srgb, #8690a8 70%, var(--text)); }\n .badge.ct-image { border-color: color-mix(in srgb, #ff9f43 55%, var(--line)); color: color-mix(in srgb, #ff9f43 75%, var(--text)); }\n\n .badge.role-user { border-color: color-mix(in srgb, #00c8ff 55%, var(--line)); color: color-mix(in srgb, #00c8ff 78%, var(--text)); }\n .badge.role-assistant { border-color: color-mix(in srgb, #ff73c4 55%, var(--line)); color: color-mix(in srgb, #ff73c4 78%, var(--text)); }\n\n .summary {\n font-size: 13px;\n line-height: 1.35;\n color: var(--text);\n word-break: break-word;\n }\n\n .summary code {\n font-family: var(--mono);\n font-size: 12px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n padding: 2px 5px;\n border-radius: 7px;\n }\n\n .detail {\n display: grid;\n grid-template-rows: auto auto auto auto 1fr;\n min-height: 560px;\n }\n\n @media (min-width: 1060px) {\n .detail {\n position: sticky;\n top: 16px;\n align-self: start;\n max-height: calc(100vh - 32px);\n }\n }\n\n .detail.inline {\n position: static;\n top: auto;\n align-self: auto;\n max-height: none;\n min-height: 0;\n margin: 8px 14px 12px;\n box-shadow: none;\n }\n\n .detail.inline pre {\n max-height: 52vh;\n }\n\n .detail-head {\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n display: grid;\n gap: 8px;\n }\n\n .detail-title {\n font-size: 14px;\n font-weight: 700;\n line-height: 1.25;\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .detail-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-start;\n }\n\n .check {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--muted);\n font-size: 12px;\n user-select: none;\n }\n\n pre {\n margin: 0;\n padding: 12px 14px 16px;\n overflow: auto;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.45;\n color: color-mix(in srgb, var(--text) 92%, transparent);\n background: transparent;\n white-space: pre-wrap;\n word-break: break-word;\n }\n\n .detail-images {\n display: flex;\n gap: 10px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--line);\n overflow-x: auto;\n overscroll-behavior-x: contain;\n }\n\n .thumb {\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n border-radius: 12px;\n padding: 0;\n cursor: pointer;\n width: 78px;\n height: 58px;\n overflow: hidden;\n flex: 0 0 auto;\n display: grid;\n place-items: stretch;\n }\n\n .thumb:hover { border-color: var(--line-2); }\n\n .thumb img {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: cover;\n }\n\n .image-modal {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: rgba(0, 0, 0, 0.68);\n display: none;\n padding: 18px;\n place-items: center;\n }\n\n .image-modal.open { display: grid; }\n\n .image-modal .dialog {\n position: relative;\n border-radius: 16px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 98%, transparent);\n box-shadow: var(--shadow);\n padding: 14px;\n max-width: min(1200px, 96vw);\n max-height: 92vh;\n display: grid;\n place-items: center;\n }\n\n .image-modal img {\n display: block;\n max-width: min(1140px, 92vw);\n max-height: 84vh;\n width: auto;\n height: auto;\n object-fit: contain;\n border-radius: 10px;\n background: color-mix(in srgb, var(--panel-2) 92%, transparent);\n }\n\n .image-modal .close {\n position: absolute;\n top: 10px;\n right: 10px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 92%, transparent);\n color: var(--text);\n border-radius: 999px;\n padding: 8px 10px;\n font: inherit;\n cursor: pointer;\n }\n\n .image-modal .close:hover { border-color: var(--line-2); }\n\n .empty {\n padding: 14px;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.35;\n }\n\n /* Pretty view styles */\n .detail-pretty {\n padding: 14px;\n border-bottom: 1px solid var(--line);\n overflow: auto;\n max-height: 50vh;\n }\n\n .detail.inline .detail-pretty {\n max-height: 40vh;\n }\n\n .pretty-section {\n margin-bottom: 14px;\n }\n\n .pretty-section:last-child {\n margin-bottom: 0;\n }\n\n .pretty-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--muted);\n margin-bottom: 6px;\n }\n\n .pretty-content {\n font-size: 13px;\n line-height: 1.5;\n }\n\n .pretty-content p {\n margin: 0 0 10px;\n }\n\n .pretty-content p:last-child {\n margin-bottom: 0;\n }\n\n .pretty-content h1, .pretty-content h2, .pretty-content h3,\n .pretty-content h4, .pretty-content h5, .pretty-content h6 {\n margin: 14px 0 8px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .pretty-content h1:first-child, .pretty-content h2:first-child,\n .pretty-content h3:first-child {\n margin-top: 0;\n }\n\n .pretty-content h1 { font-size: 1.4em; }\n .pretty-content h2 { font-size: 1.25em; }\n .pretty-content h3 { font-size: 1.1em; }\n\n .pretty-content ul, .pretty-content ol {\n margin: 8px 0;\n padding-left: 1.5em;\n list-style-position: outside;\n }\n\n .pretty-content li {\n margin: 4px 0;\n }\n\n .thinking-block ul, .thinking-block ol {\n padding-left: 2em;\n }\n\n .pretty-content code {\n font-family: var(--mono);\n font-size: 0.9em;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n padding: 2px 5px;\n border-radius: 5px;\n }\n\n .pretty-content pre {\n margin: 10px 0;\n padding: 12px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n overflow-x: auto;\n }\n\n .pretty-content pre code {\n background: none;\n border: none;\n padding: 0;\n font-size: 12px;\n }\n\n .pretty-content blockquote {\n margin: 10px 0;\n padding: 8px 12px;\n border-left: 3px solid var(--accent);\n background: color-mix(in srgb, var(--accent) 8%, transparent);\n border-radius: 0 8px 8px 0;\n }\n\n .pretty-content a {\n color: var(--accent);\n text-decoration: none;\n }\n\n .pretty-content a:hover {\n text-decoration: underline;\n }\n\n .pretty-content strong {\n font-weight: 600;\n }\n\n .pretty-content em {\n font-style: italic;\n }\n\n .pretty-content table {\n border-collapse: collapse;\n margin: 10px 0;\n width: 100%;\n font-size: 13px;\n }\n\n .pretty-content th,\n .pretty-content td {\n border: 1px solid var(--line);\n padding: 8px 10px;\n text-align: left;\n }\n\n .pretty-content th {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n font-weight: 600;\n }\n\n .pretty-content tr:hover {\n background: color-mix(in srgb, var(--accent) 6%, transparent);\n }\n\n .command-block {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n overflow: hidden;\n margin: 8px 0;\n }\n\n .command-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-bottom: 1px solid var(--line);\n font-size: 12px;\n }\n\n .command-header .tool-name {\n font-weight: 600;\n color: var(--accent);\n }\n\n .command-header .tool-desc {\n color: var(--muted);\n }\n\n .command-input {\n padding: 10px 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n border-bottom: 1px solid var(--line);\n }\n\n .command-input::before {\n content: '$ ';\n color: var(--good);\n font-weight: 600;\n }\n\n .command-output {\n padding: 10px 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 300px;\n overflow: auto;\n color: var(--muted);\n }\n\n .file-path {\n font-family: var(--mono);\n font-size: 12px;\n padding: 6px 10px;\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 8px;\n display: inline-block;\n margin: 4px 0;\n }\n\n .tool-params {\n display: grid;\n gap: 8px;\n margin-top: 8px;\n }\n\n .tool-param {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 8px;\n align-items: start;\n font-size: 12px;\n }\n\n .tool-param-name {\n font-family: var(--mono);\n font-weight: 600;\n color: var(--accent);\n padding: 2px 6px;\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-radius: 4px;\n }\n\n .tool-param-value {\n font-family: var(--mono);\n white-space: pre-wrap;\n word-break: break-word;\n padding: 2px 0;\n }\n\n .thinking-block {\n background: color-mix(in srgb, var(--accent-2) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--accent-2) 30%, var(--line));\n border-radius: 10px;\n padding: 12px 16px;\n font-size: 13px;\n line-height: 1.5;\n }\n\n .result-block {\n background: color-mix(in srgb, var(--panel-2) 85%, transparent);\n border: 1px solid var(--line);\n border-radius: 10px;\n padding: 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 400px;\n overflow: auto;\n }\n\n .result-error {\n border-color: color-mix(in srgb, var(--bad) 50%, var(--line));\n background: color-mix(in srgb, var(--bad) 8%, transparent);\n }\n\n .json-toggle {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 14px;\n border-bottom: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel-2) 50%, transparent);\n }\n\n .json-toggle-label {\n font-size: 12px;\n color: var(--muted);\n font-weight: 500;\n }\n\n .prompts-modal {\n position: fixed;\n inset: 0;\n z-index: 80;\n background: rgba(0, 0, 0, 0.68);\n display: none;\n padding: 18px;\n place-items: center;\n }\n\n .prompts-modal.open { display: grid; }\n\n .prompts-modal .dialog {\n position: relative;\n border-radius: 16px;\n border: 1px solid var(--line);\n background: color-mix(in srgb, var(--panel) 98%, transparent);\n box-shadow: var(--shadow);\n padding: 14px;\n width: min(1100px, 96vw);\n height: min(86vh, 780px);\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: 10px;\n }\n\n .prompts-head {\n display: flex;\n gap: 10px;\n align-items: center;\n justify-content: space-between;\n }\n\n .prompts-title {\n font-weight: 700;\n letter-spacing: 0.2px;\n }\n\n .prompts-modal textarea {\n width: 100%;\n height: 100%;\n resize: none;\n border-radius: 12px;\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.45;\n padding: 12px;\n }\n\n .prompts-actions {\n display: flex;\n gap: 10px;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n }\n </style>\n</head>\n <body>\n <div class=\"wrap\">\n <header>\n <div>\n <h1>Claude Code Timeline Viewer</h1>\n <p class=\"sub\">Open a Claude Code session <code>.jsonl</code> file and explore it as an interactive timeline. Times are shown in your local timezone by default. Find sessions in <code>~/.claude/projects/</code>.</p>\n </div>\n </header>\n\n <section class=\"panel input-panel\" aria-label=\"Input\">\n <div class=\"input-grid\">\n <div>\n <div id=\"dropzone\" class=\"dropzone\" tabindex=\"0\">\n <strong>Drop a <code>.jsonl</code> file here</strong>\n <div class=\"muted\">or click to choose a file</div>\n <div class=\"tiny\">Supported: file picker, drag-and-drop, paste, or URL fetch (CORS-enabled).</div>\n <input id=\"file-input\" type=\"file\" accept=\".jsonl,application/json,text/plain\" hidden />\n </div>\n </div>\n <div>\n <div class=\"tabs\" role=\"tablist\" aria-label=\"Input tabs\">\n <button type=\"button\" class=\"tab active\" data-tab=\"paste\" aria-selected=\"true\">Paste</button>\n <button type=\"button\" class=\"tab\" data-tab=\"url\" aria-selected=\"false\">URL</button>\n </div>\n\n <div id=\"panel-paste\" data-panel=\"paste\">\n <textarea id=\"paste-input\" placeholder=\"Paste JSONL content here...\"></textarea>\n <div class=\"row\">\n <button type=\"button\" class=\"btn primary\" id=\"render-paste\">Render pasted content</button>\n <div class=\"tiny\">Tip: the timeline uses one JSON object per line.</div>\n </div>\n </div>\n\n <div id=\"panel-url\" data-panel=\"url\" hidden>\n <input id=\"url-input\" type=\"url\" placeholder=\"https://example.com/session.jsonl\" />\n <div class=\"row\">\n <button type=\"button\" class=\"btn primary\" id=\"fetch-url\">Fetch and render</button>\n <div class=\"tiny\">The URL must allow cross-origin requests (CORS).</div>\n </div>\n </div>\n </div>\n </div>\n\n <div id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></div>\n </section>\n\n <section class=\"panel filters-panel\" aria-label=\"Filters\">\n <div class=\"controls\" aria-label=\"Timeline controls\">\n <div class=\"field\" style=\"min-width: 170px\">\n <div class=\"label\">Timezone</div>\n <div class=\"segmented\" role=\"group\" aria-label=\"Timezone toggle\">\n <button type=\"button\" id=\"tz-local\" class=\"active\">Local</button>\n <button type=\"button\" id=\"tz-utc\">UTC</button>\n </div>\n </div>\n <div class=\"field\" style=\"min-width: 220px\">\n <div class=\"label\">Search</div>\n <input id=\"search\" type=\"search\" placeholder=\"message, tool name, cwd...\" autocomplete=\"off\" />\n </div>\n <div class=\"field\" style=\"min-width: 190px\">\n <div class=\"label\">Type</div>\n <select id=\"type-filter\"></select>\n </div>\n <div class=\"field\" style=\"min-width: 210px\">\n <div class=\"label\">Content Type</div>\n <select id=\"content-type-filter\"></select>\n </div>\n <div class=\"field\" style=\"min-width: 160px\">\n <div class=\"label\">Role</div>\n <select id=\"role-filter\"></select>\n </div>\n <label class=\"check\" title=\"Hide thinking blocks (can be verbose)\">\n <input type=\"checkbox\" id=\"hide-thinking\" />\n Hide thinking\n </label>\n <button type=\"button\" class=\"btn\" id=\"clear-filters\">Clear</button>\n </div>\n </section>\n\n <div class=\"controls\" style=\"margin-bottom: 14px;\">\n <button type=\"button\" class=\"btn\" id=\"show-prompts\" disabled>Show prompts</button>\n </div>\n\n <main>\n <section class=\"panel timeline-panel\" aria-label=\"Timeline\">\n <div class=\"stats\">\n <div class=\"big\"><span id=\"stats-showing\">0</span> <span class=\"muted\">showing</span> <span class=\"muted\">/</span> <span id=\"stats-total\">0</span> <span class=\"muted\">events</span></div>\n <div class=\"muted\" id=\"stats-range\">-</div>\n </div>\n <div id=\"timeline\" class=\"timeline\" role=\"list\" tabindex=\"0\" aria-label=\"Events list\"></div>\n </section>\n\n <aside class=\"panel detail\" aria-label=\"Details\">\n <div class=\"detail-head\">\n <div class=\"detail-title\" id=\"detail-title\">Select an event</div>\n <div class=\"detail-actions\">\n <button type=\"button\" class=\"btn\" id=\"copy-json\" disabled>Copy JSON</button>\n <button type=\"button\" class=\"btn\" id=\"copy-raw\" disabled>Copy raw line</button>\n <label class=\"check\" title=\"Truncate very long strings in the JSON preview\">\n <input type=\"checkbox\" id=\"truncate-strings\" checked />\n Truncate long strings\n </label>\n </div>\n </div>\n <div id=\"detail-images\" class=\"detail-images\" hidden aria-label=\"Event images\"></div>\n <div id=\"detail-pretty\" class=\"detail-pretty\" hidden aria-label=\"Formatted event view\"></div>\n <div class=\"json-toggle\">\n <span class=\"json-toggle-label\">Raw JSON</span>\n </div>\n <pre id=\"detail-json\" aria-label=\"Selected event JSON\" class=\"empty\">Load a file to get started.</pre>\n </aside>\n </main>\n </div>\n\n <div id=\"image-modal\" class=\"image-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\">\n <div class=\"dialog\">\n <button type=\"button\" class=\"close\" id=\"image-modal-close\" aria-label=\"Close image\">Close</button>\n <img id=\"image-modal-img\" alt=\"\" />\n </div>\n </div>\n\n <div id=\"prompts-modal\" class=\"prompts-modal\" role=\"dialog\" aria-modal=\"true\" aria-hidden=\"true\" aria-label=\"User prompts\">\n <div class=\"dialog\">\n <div class=\"prompts-head\">\n <div class=\"prompts-title\">User prompts</div>\n <button type=\"button\" class=\"btn\" id=\"prompts-close\" aria-label=\"Close prompts\">Close</button>\n </div>\n <textarea id=\"prompts-text\" readonly spellcheck=\"false\"></textarea>\n <div class=\"prompts-actions\">\n <button type=\"button\" class=\"btn primary\" id=\"prompts-copy\">Copy</button>\n </div>\n </div>\n </div>\n\n <script>\n const els = {\n dropzone: document.getElementById('dropzone'),\n fileInput: document.getElementById('file-input'),\n pasteInput: document.getElementById('paste-input'),\n renderPaste: document.getElementById('render-paste'),\n urlInput: document.getElementById('url-input'),\n fetchUrl: document.getElementById('fetch-url'),\n status: document.getElementById('status'),\n timeline: document.getElementById('timeline'),\n detailPanel: document.querySelector('aside.detail'),\n detailTitle: document.getElementById('detail-title'),\n detailImages: document.getElementById('detail-images'),\n detailPretty: document.getElementById('detail-pretty'),\n detailJson: document.getElementById('detail-json'),\n copyJson: document.getElementById('copy-json'),\n copyRaw: document.getElementById('copy-raw'),\n truncateStrings: document.getElementById('truncate-strings'),\n tzLocal: document.getElementById('tz-local'),\n tzUtc: document.getElementById('tz-utc'),\n search: document.getElementById('search'),\n typeFilter: document.getElementById('type-filter'),\n contentTypeFilter: document.getElementById('content-type-filter'),\n roleFilter: document.getElementById('role-filter'),\n hideThinking: document.getElementById('hide-thinking'),\n statsShowing: document.getElementById('stats-showing'),\n statsTotal: document.getElementById('stats-total'),\n statsRange: document.getElementById('stats-range'),\n clearFilters: document.getElementById('clear-filters'),\n imageModal: document.getElementById('image-modal'),\n imageModalImg: document.getElementById('image-modal-img'),\n imageModalClose: document.getElementById('image-modal-close'),\n showPrompts: document.getElementById('show-prompts'),\n promptsModal: document.getElementById('prompts-modal'),\n promptsClose: document.getElementById('prompts-close'),\n promptsText: document.getElementById('prompts-text'),\n promptsCopy: document.getElementById('prompts-copy'),\n };\n\n const state = {\n sourceLabel: null,\n tzMode: 'local',\n events: [],\n filtered: [],\n selectedId: null,\n selected: null,\n filters: {\n query: '',\n type: 'all',\n contentType: 'all',\n role: 'all',\n hideThinking: false,\n },\n };\n\n let lastActiveElementBeforeModal = null;\n let lastBodyOverflow = null;\n let lastActiveElementBeforePrompts = null;\n let lastBodyOverflowBeforePrompts = null;\n let promptsCopiedTimer = null;\n let suppressUrlStateSync = false;\n const wideLayoutMq = window.matchMedia('(min-width: 1060px)');\n const detailDock = document.createElement('div');\n detailDock.id = 'detail-dock';\n detailDock.style.display = 'none';\n els.detailPanel.parentElement.insertBefore(detailDock, els.detailPanel);\n\n function updateDetailPlacement() {\n const inline = !wideLayoutMq.matches;\n\n if (!inline) {\n if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n els.detailPanel.classList.remove('inline');\n return;\n }\n\n const selectedNode = els.timeline.querySelector('.event.selected');\n if (!selectedNode) {\n if (els.detailPanel.previousSibling !== detailDock) detailDock.after(els.detailPanel);\n els.detailPanel.classList.remove('inline');\n return;\n }\n\n if (selectedNode.nextSibling !== els.detailPanel) selectedNode.after(els.detailPanel);\n els.detailPanel.classList.add('inline');\n }\n\n function setStatus(message, kind = 'good') {\n if (!message) {\n els.status.className = 'status';\n els.status.textContent = '';\n return;\n }\n els.status.className = `status visible ${kind}`;\n els.status.textContent = message;\n }\n\n function pad2(number) {\n return String(number).padStart(2, '0');\n }\n\n function pad3(number) {\n return String(number).padStart(3, '0');\n }\n\n function getOffsetString(date) {\n const offsetMinutes = -date.getTimezoneOffset();\n const sign = offsetMinutes >= 0 ? '+' : '-';\n const abs = Math.abs(offsetMinutes);\n const hours = Math.floor(abs / 60);\n const minutes = abs % 60;\n return `${sign}${pad2(hours)}:${pad2(minutes)}`;\n }\n\n function formatTimestamp(date, tzMode) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n if (tzMode === 'utc') {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +\n `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}.` +\n `${pad3(date.getUTCMilliseconds())}Z`;\n }\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ` +\n `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}.` +\n `${pad3(date.getMilliseconds())} ${getOffsetString(date)}`;\n }\n\n function dayKey(date, tzMode) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '-';\n if (tzMode === 'utc') {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;\n }\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n }\n\n function formatDelta(ms) {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return '';\n const abs = Math.abs(ms);\n const sign = ms < 0 ? '-' : '+';\n if (abs < 1000) return `${sign}${Math.round(abs)}ms`;\n if (abs < 60_000) return `${sign}${(abs / 1000).toFixed(abs < 10_000 ? 2 : 1)}s`;\n if (abs < 3_600_000) {\n const m = Math.floor(abs / 60_000);\n const s = Math.floor((abs % 60_000) / 1000);\n return `${sign}${m}m ${pad2(s)}s`;\n }\n if (abs < 86_400_000) {\n const h = Math.floor(abs / 3_600_000);\n const m = Math.floor((abs % 3_600_000) / 60_000);\n return `${sign}${h}h ${m}m`;\n }\n const d = Math.floor(abs / 86_400_000);\n const h = Math.floor((abs % 86_400_000) / 3_600_000);\n return `${sign}${d}d ${h}h`;\n }\n\n function safeJsonParse(text) {\n try {\n return { ok: true, value: JSON.parse(text) };\n } catch (error) {\n return { ok: false, error };\n }\n }\n\n function extractMessageText(message) {\n if (!message || typeof message !== 'object') return null;\n const content = message.content;\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return null;\n const parts = [];\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n if (typeof item.text === 'string') parts.push(item.text);\n else if (typeof item.thinking === 'string') parts.push(item.thinking);\n }\n const joined = parts.join('');\n return joined || null;\n }\n\n function getContentTypes(message) {\n if (!message || typeof message !== 'object') return [];\n const content = message.content;\n if (typeof content === 'string') return ['text'];\n if (!Array.isArray(content)) return [];\n const types = new Set();\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n if (item.type) types.add(item.type);\n }\n return Array.from(types);\n }\n\n function firstLine(text) {\n if (typeof text !== 'string') return null;\n const trimmed = text.trim();\n if (!trimmed) return null;\n const idx = trimmed.indexOf('\\n');\n return idx === -1 ? trimmed : trimmed.slice(0, idx).trim();\n }\n\n function normalizeSummaryText(text, maxLen = 260) {\n if (typeof text !== 'string') return '';\n const singleLine = text.replace(/\\s+/g, ' ').trim();\n if (singleLine.length <= maxLen) return singleLine;\n return singleLine.slice(0, maxLen - 1) + '...';\n }\n\n function summarizeEvent(obj) {\n const kind = obj?.type || '';\n const message = obj?.message;\n\n if (kind === 'file-history-snapshot') {\n const msgId = obj?.messageId ? `msg=${obj.messageId.slice(0, 8)}...` : null;\n return normalizeSummaryText(msgId || 'file-history-snapshot');\n }\n\n if (kind === 'user' || kind === 'assistant') {\n const role = message?.role || kind;\n const content = message?.content;\n\n if (typeof content === 'string') {\n return normalizeSummaryText(`${role}: ${content}`);\n }\n\n if (Array.isArray(content) && content.length > 0) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n return normalizeSummaryText(`${role}: ${item.text}`);\n }\n if (item?.type === 'thinking' && item.thinking) {\n return normalizeSummaryText(`${role} (thinking): ${item.thinking}`);\n }\n if (item?.type === 'tool_use') {\n const name = item.name || 'tool';\n const input = item.input;\n if (name === 'Bash' && input?.command) {\n return normalizeSummaryText(`${name}: ${input.command}`);\n }\n if (name === 'Read' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Write' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Edit' && input?.file_path) {\n return normalizeSummaryText(`${name}: ${input.file_path}`);\n }\n if (name === 'Glob' && input?.pattern) {\n return normalizeSummaryText(`${name}: ${input.pattern}`);\n }\n if (name === 'Grep' && input?.pattern) {\n return normalizeSummaryText(`${name}: ${input.pattern}`);\n }\n if (name === 'Task' && input?.description) {\n return normalizeSummaryText(`${name}: ${input.description}`);\n }\n if (input && typeof input === 'object') {\n const keys = Object.keys(input).slice(0, 3);\n return normalizeSummaryText(`${name}(${keys.join(', ')}${Object.keys(input).length > 3 ? ', ...' : ''})`);\n }\n return normalizeSummaryText(`${name}()`);\n }\n if (item?.type === 'tool_result') {\n const toolUseId = item.tool_use_id ? item.tool_use_id.slice(0, 12) : 'result';\n const resultContent = item.content;\n if (typeof resultContent === 'string') {\n return normalizeSummaryText(`tool_result: ${firstLine(resultContent) || toolUseId}`);\n }\n if (Array.isArray(resultContent)) {\n for (const rc of resultContent) {\n if (rc?.type === 'text' && rc.text) {\n return normalizeSummaryText(`tool_result: ${firstLine(rc.text) || toolUseId}`);\n }\n if (rc?.type === 'image') {\n return normalizeSummaryText(`tool_result: [image]`);\n }\n }\n }\n return normalizeSummaryText(`tool_result: ${toolUseId}`);\n }\n if (item?.type === 'image') {\n return normalizeSummaryText(`${role}: [image]`);\n }\n }\n return normalizeSummaryText(`${role}: [${content.length} content items]`);\n }\n\n return kind;\n }\n\n return kind || 'event';\n }\n\n function buildSearchText(obj, summary) {\n const pieces = [];\n pieces.push(obj?.type || '');\n pieces.push(obj?.uuid || '');\n pieces.push(obj?.sessionId || '');\n pieces.push(obj?.cwd || '');\n pieces.push(obj?.gitBranch || '');\n pieces.push(obj?.slug || '');\n const message = obj?.message;\n if (message && typeof message === 'object') {\n pieces.push(message.role || '');\n const msg = extractMessageText(message);\n if (msg) pieces.push(msg);\n const content = message.content;\n if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'tool_use') {\n pieces.push(item.name || '');\n if (item.input && typeof item.input === 'object') {\n pieces.push(JSON.stringify(item.input).slice(0, 500));\n }\n }\n if (item?.type === 'tool_result' && typeof item.content === 'string') {\n pieces.push(item.content.slice(0, 500));\n }\n }\n }\n }\n if (typeof summary === 'string') pieces.push(summary);\n return pieces.join('\\n').toLowerCase();\n }\n\n function stringifyJson(value, truncateLongStrings) {\n const maxLen = 8000;\n return JSON.stringify(value, (key, v) => {\n if (!truncateLongStrings) return v;\n if (typeof v === 'string' && v.length > maxLen) {\n return v.slice(0, maxLen) + `... [truncated ${v.length - maxLen} chars]`;\n }\n return v;\n }, 2);\n }\n\n function updateFilterOptions(events) {\n const typeSet = new Set();\n const contentTypeSet = new Set();\n const roleSet = new Set();\n for (const e of events) {\n if (e.kind) typeSet.add(e.kind);\n for (const ct of e.contentTypes) {\n contentTypeSet.add(ct);\n }\n if (!e.contentTypes.length) contentTypeSet.add('(none)');\n if (e.role) roleSet.add(e.role);\n }\n\n function setOptions(select, items, current) {\n select.innerHTML = '';\n const optAll = document.createElement('option');\n optAll.value = 'all';\n optAll.textContent = 'All';\n select.appendChild(optAll);\n for (const item of items) {\n const opt = document.createElement('option');\n opt.value = item;\n opt.textContent = item;\n select.appendChild(opt);\n }\n select.value = current;\n }\n\n setOptions(els.typeFilter, Array.from(typeSet).sort(), state.filters.type);\n setOptions(els.contentTypeFilter, Array.from(contentTypeSet).sort(), state.filters.contentType);\n setOptions(els.roleFilter, Array.from(roleSet).sort(), state.filters.role);\n\n if (events.length) {\n state.filters.type = els.typeFilter.value || 'all';\n state.filters.contentType = els.contentTypeFilter.value || 'all';\n state.filters.role = els.roleFilter.value || 'all';\n }\n }\n\n function readHashParams() {\n const raw = window.location.hash && window.location.hash.startsWith('#')\n ? window.location.hash.slice(1)\n : '';\n return new URLSearchParams(raw);\n }\n\n function writeHashParams(params) {\n const base = window.location.href.split('#')[0];\n const hash = params.toString();\n const next = base + (hash ? `#${hash}` : '');\n window.history.replaceState(null, '', next);\n }\n\n function syncUrlHashFromControls({ clearSel = false } = {}) {\n if (suppressUrlStateSync) return;\n const params = readHashParams();\n params.set('tz', state.tzMode);\n params.set('q', (els.search.value || '').trim());\n params.set('type', state.filters.type || 'all');\n params.set('ct', state.filters.contentType || 'all');\n params.set('role', state.filters.role || 'all');\n params.set('hide', state.filters.hideThinking ? '1' : '0');\n params.set('truncate', els.truncateStrings.checked ? '1' : '0');\n const selectedLine = state.selected?.line;\n if (typeof selectedLine === 'number' && Number.isFinite(selectedLine)) {\n params.set('sel', String(selectedLine));\n } else if (clearSel) {\n params.delete('sel');\n }\n writeHashParams(params);\n }\n\n function applySelectionFromHash() {\n if (!state.events.length) return;\n const params = readHashParams();\n const raw = params.get('sel');\n if (!raw) return;\n const line = Number(raw);\n if (!Number.isFinite(line)) return;\n const found = state.events.find(e => e.line === line);\n if (!found) return;\n if (state.selectedId === found.id) return;\n suppressUrlStateSync = true;\n try {\n selectEvent(found.id, { scroll: true });\n } finally {\n suppressUrlStateSync = false;\n }\n }\n\n function applyControlsFromHash() {\n const params = readHashParams();\n suppressUrlStateSync = true;\n try {\n const q = params.get('q');\n if (q !== null) {\n els.search.value = q;\n state.filters.query = q.trim().toLowerCase();\n }\n\n const type = params.get('type');\n if (type !== null) state.filters.type = type;\n const ct = params.get('ct');\n if (ct !== null) state.filters.contentType = ct;\n const role = params.get('role');\n if (role !== null) state.filters.role = role;\n\n const hide = params.get('hide');\n if (hide !== null) {\n els.hideThinking.checked = hide === '1' || hide === 'true';\n state.filters.hideThinking = els.hideThinking.checked;\n }\n\n const truncate = params.get('truncate');\n if (truncate !== null) {\n els.truncateStrings.checked = truncate === '1' || truncate === 'true';\n }\n\n const tz = params.get('tz');\n if (tz === 'utc' || tz === 'local') {\n state.tzMode = tz;\n els.tzLocal.classList.toggle('active', tz === 'local');\n els.tzUtc.classList.toggle('active', tz === 'utc');\n }\n } finally {\n suppressUrlStateSync = false;\n }\n }\n\n function applyFilters() {\n const query = state.filters.query;\n const type = state.filters.type;\n const contentType = state.filters.contentType;\n const role = state.filters.role;\n const hideThinking = state.filters.hideThinking;\n\n const out = [];\n for (const e of state.events) {\n if (hideThinking && e.contentTypes.includes('thinking') && e.contentTypes.length === 1) continue;\n if (type !== 'all' && e.kind !== type) continue;\n if (contentType !== 'all') {\n const ct = e.contentTypes.length ? e.contentTypes : ['(none)'];\n if (!ct.includes(contentType)) continue;\n }\n if (role !== 'all' && e.role !== role) continue;\n if (query && !e.search.includes(query)) continue;\n out.push(e);\n }\n\n state.filtered = out;\n els.statsShowing.textContent = String(out.length);\n els.statsTotal.textContent = String(state.events.length);\n\n const start = out.find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n const end = [...out].reverse().find(e => e.date instanceof Date && !Number.isNaN(e.date.getTime()))?.date;\n if (start && end) {\n els.statsRange.textContent = `${formatTimestamp(start, state.tzMode)} -> ${formatTimestamp(end, state.tzMode)}`;\n } else {\n els.statsRange.textContent = '-';\n }\n\n renderTimeline();\n }\n\n function clearSelection({ syncHash = true } = {}) {\n state.selectedId = null;\n state.selected = null;\n els.detailTitle.textContent = 'Select an event';\n renderDetailImages(null);\n renderPrettyView(null);\n els.detailJson.textContent = state.events.length ? 'Select an event from the timeline.' : 'Load a file to get started.';\n els.detailJson.classList.add('empty');\n els.copyJson.disabled = true;\n els.copyRaw.disabled = true;\n for (const node of els.timeline.querySelectorAll('.event.selected')) {\n node.classList.remove('selected');\n }\n updateDetailPlacement();\n if (syncHash) syncUrlHashFromControls({ clearSel: true });\n }\n\n function renderTimeline() {\n els.timeline.innerHTML = '';\n\n if (!state.filtered.length) {\n const empty = document.createElement('div');\n empty.className = 'empty';\n empty.textContent = state.events.length ? 'No events match your filters.' : 'Load a file to see a timeline.';\n els.timeline.appendChild(empty);\n clearSelection();\n return;\n }\n\n const fragment = document.createDocumentFragment();\n let lastDay = null;\n let prevDate = null;\n\n for (const e of state.filtered) {\n const day = dayKey(e.date, state.tzMode);\n if (day !== lastDay) {\n lastDay = day;\n const sep = document.createElement('div');\n sep.className = 'day-sep';\n sep.textContent = day;\n fragment.appendChild(sep);\n }\n\n const row = document.createElement('div');\n row.className = 'event';\n row.setAttribute('role', 'listitem');\n row.dataset.eventId = String(e.id);\n\n const time = document.createElement('div');\n time.className = 'time';\n\n const ts = document.createElement('div');\n ts.className = 'ts';\n ts.textContent = formatTimestamp(e.date, state.tzMode);\n ts.dataset.kind = 'timestamp';\n\n const meta = document.createElement('div');\n meta.textContent = [\n `#${e.line}`,\n (prevDate && e.date) ? formatDelta(e.date.getTime() - prevDate.getTime()) : null,\n ].filter(Boolean).join(' . ');\n\n time.appendChild(ts);\n time.appendChild(meta);\n\n const body = document.createElement('div');\n\n const badges = document.createElement('div');\n badges.className = 'badges';\n const badgeKind = document.createElement('span');\n badgeKind.className = `badge k-${e.kind}`;\n badgeKind.textContent = e.kind;\n badges.appendChild(badgeKind);\n\n for (const ct of e.contentTypes.slice(0, 2)) {\n const badgeCt = document.createElement('span');\n badgeCt.className = `badge ct-${ct}`;\n badgeCt.textContent = ct;\n badges.appendChild(badgeCt);\n }\n if (e.contentTypes.length > 2) {\n const badgeMore = document.createElement('span');\n badgeMore.className = 'badge';\n badgeMore.textContent = `+${e.contentTypes.length - 2}`;\n badges.appendChild(badgeMore);\n }\n\n if (e.role) {\n const badgeRole = document.createElement('span');\n badgeRole.className = `badge role-${e.role}`;\n badgeRole.textContent = e.role;\n badges.appendChild(badgeRole);\n }\n\n const summary = document.createElement('div');\n summary.className = 'summary';\n summary.textContent = e.summary;\n\n body.appendChild(badges);\n body.appendChild(summary);\n\n row.appendChild(time);\n row.appendChild(body);\n\n row.addEventListener('click', () => selectEvent(e.id, { focusTimeline: true }));\n fragment.appendChild(row);\n\n if (e.date instanceof Date && !Number.isNaN(e.date.getTime())) prevDate = e.date;\n }\n\n els.timeline.appendChild(fragment);\n\n if (state.selectedId !== null) {\n const stillThere = state.filtered.some(e => e.id === state.selectedId);\n if (!stillThere) clearSelection();\n else selectEvent(state.selectedId, { scroll: false });\n } else {\n clearSelection({ syncHash: false });\n }\n }\n\n function selectEvent(id, { scroll = false, focusTimeline = false } = {}) {\n const e = state.filtered.find(item => item.id === id) || state.events.find(item => item.id === id);\n if (!e) return;\n state.selectedId = id;\n state.selected = e;\n\n if (focusTimeline) {\n els.timeline.focus({ preventScroll: true });\n }\n\n for (const node of els.timeline.querySelectorAll('.event.selected')) {\n node.classList.remove('selected');\n }\n const node = els.timeline.querySelector(`.event[data-event-id=\"${CSS.escape(String(id))}\"]`);\n if (node) {\n node.classList.add('selected');\n updateDetailPlacement();\n if (scroll) node.scrollIntoView({ block: 'center' });\n }\n\n els.detailJson.classList.remove('empty');\n const titleParts = [\n formatTimestamp(e.date, state.tzMode),\n `#${e.line}`,\n e.kind,\n e.role ? `(${e.role})` : null,\n ].filter(Boolean);\n els.detailTitle.textContent = titleParts.join(' ');\n\n const shouldTruncate = els.truncateStrings.checked;\n renderDetailImages(e.obj);\n renderPrettyView(e.obj);\n els.detailJson.textContent = stringifyJson(e.obj, shouldTruncate) || '';\n els.copyJson.disabled = false;\n els.copyRaw.disabled = false;\n updateDetailPlacement();\n syncUrlHashFromControls();\n }\n\n function extractImageUrls(obj) {\n const urls = [];\n const message = obj?.message;\n if (!message || typeof message !== 'object') return urls;\n const content = message.content;\n if (!Array.isArray(content)) return urls;\n\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n\n // Direct image in content\n if (item.type === 'image' && item.source?.type === 'base64' && item.source?.data) {\n const mediaType = item.source.media_type || 'image/png';\n urls.push(`data:${mediaType};base64,${item.source.data}`);\n }\n\n // Image in tool_result content\n if (item.type === 'tool_result' && Array.isArray(item.content)) {\n for (const rc of item.content) {\n if (rc?.type === 'image' && rc.source?.type === 'base64' && rc.source?.data) {\n const mediaType = rc.source.media_type || 'image/png';\n urls.push(`data:${mediaType};base64,${rc.source.data}`);\n }\n }\n }\n }\n\n return urls;\n }\n\n function escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n // Initialize markdown-it with GFM-like settings\n const md = window.markdownit({\n html: false, // Disable raw HTML input\n linkify: true, // Auto-convert URLs to links\n typographer: true, // Smart quotes, dashes\n breaks: true // Convert \\n to <br>\n });\n\n function renderMarkdown(text) {\n if (typeof text !== 'string') return '';\n\n // Render markdown to HTML\n const html = md.render(text);\n\n // Sanitize with DOMPurify to prevent XSS\n return DOMPurify.sanitize(html);\n }\n\n function renderPrettyView(obj) {\n els.detailPretty.innerHTML = '';\n\n if (!obj || typeof obj !== 'object') {\n els.detailPretty.hidden = true;\n return;\n }\n\n const message = obj.message;\n if (!message || typeof message !== 'object') {\n els.detailPretty.hidden = true;\n return;\n }\n\n const content = message.content;\n const fragment = document.createDocumentFragment();\n let hasContent = false;\n\n // Handle string content (simple text message)\n if (typeof content === 'string') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = message.role || 'Message';\n const body = document.createElement('div');\n body.className = 'pretty-content';\n body.innerHTML = renderMarkdown(content);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Handle array content\n if (Array.isArray(content)) {\n for (const item of content) {\n if (!item || typeof item !== 'object') continue;\n\n // Text content\n if (item.type === 'text' && item.text) {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = message.role === 'assistant' ? 'Response' : 'Message';\n const body = document.createElement('div');\n body.className = 'pretty-content';\n body.innerHTML = renderMarkdown(item.text);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Thinking content\n if (item.type === 'thinking' && item.thinking) {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = 'Thinking';\n const body = document.createElement('div');\n body.className = 'thinking-block pretty-content';\n body.innerHTML = renderMarkdown(item.thinking);\n section.appendChild(label);\n section.appendChild(body);\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Tool use\n if (item.type === 'tool_use') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = 'Tool Call';\n\n const toolName = item.name || 'Unknown Tool';\n const input = item.input || {};\n\n // Special handling for Bash command\n if (toolName === 'Bash' && input.command) {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n if (input.description) {\n const descSpan = document.createElement('span');\n descSpan.className = 'tool-desc';\n descSpan.textContent = input.description;\n header.appendChild(descSpan);\n }\n\n const cmdInput = document.createElement('div');\n cmdInput.className = 'command-input';\n cmdInput.textContent = input.command;\n\n block.appendChild(header);\n block.appendChild(cmdInput);\n section.appendChild(label);\n section.appendChild(block);\n }\n // Special handling for file operations\n else if (['Read', 'Write', 'Edit', 'Glob', 'Grep'].includes(toolName)) {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n\n block.appendChild(header);\n\n const params = document.createElement('div');\n params.className = 'tool-params';\n params.style.padding = '10px 12px';\n\n for (const [key, value] of Object.entries(input)) {\n const param = document.createElement('div');\n param.className = 'tool-param';\n const paramName = document.createElement('span');\n paramName.className = 'tool-param-name';\n paramName.textContent = key;\n const paramValue = document.createElement('span');\n paramValue.className = 'tool-param-value';\n if (typeof value === 'string') {\n paramValue.textContent = value.length > 500 ? value.slice(0, 500) + '...' : value;\n } else {\n paramValue.textContent = JSON.stringify(value, null, 2);\n }\n param.appendChild(paramName);\n param.appendChild(paramValue);\n params.appendChild(param);\n }\n\n block.appendChild(params);\n section.appendChild(label);\n section.appendChild(block);\n }\n // Generic tool display\n else {\n const block = document.createElement('div');\n block.className = 'command-block';\n\n const header = document.createElement('div');\n header.className = 'command-header';\n const nameSpan = document.createElement('span');\n nameSpan.className = 'tool-name';\n nameSpan.textContent = toolName;\n header.appendChild(nameSpan);\n\n block.appendChild(header);\n\n if (Object.keys(input).length > 0) {\n const params = document.createElement('div');\n params.className = 'tool-params';\n params.style.padding = '10px 12px';\n\n for (const [key, value] of Object.entries(input)) {\n const param = document.createElement('div');\n param.className = 'tool-param';\n const paramName = document.createElement('span');\n paramName.className = 'tool-param-name';\n paramName.textContent = key;\n const paramValue = document.createElement('span');\n paramValue.className = 'tool-param-value';\n if (typeof value === 'string') {\n paramValue.textContent = value.length > 1000 ? value.slice(0, 1000) + '...' : value;\n } else {\n paramValue.textContent = JSON.stringify(value, null, 2);\n }\n param.appendChild(paramName);\n param.appendChild(paramValue);\n params.appendChild(param);\n }\n\n block.appendChild(params);\n }\n\n section.appendChild(label);\n section.appendChild(block);\n }\n\n fragment.appendChild(section);\n hasContent = true;\n }\n\n // Tool result\n if (item.type === 'tool_result') {\n const section = document.createElement('div');\n section.className = 'pretty-section';\n const label = document.createElement('div');\n label.className = 'pretty-label';\n label.textContent = item.is_error ? 'Tool Error' : 'Tool Result';\n\n const resultContent = item.content;\n let resultText = '';\n\n if (typeof resultContent === 'string') {\n resultText = resultContent;\n } else if (Array.isArray(resultContent)) {\n for (const rc of resultContent) {\n if (rc?.type === 'text' && rc.text) {\n resultText += rc.text;\n }\n }\n }\n\n if (resultText) {\n const block = document.createElement('div');\n block.className = 'pretty-content' + (item.is_error ? ' result-error' : '');\n const displayText = resultText.length > 5000 ? resultText.slice(0, 5000) + '\\n... [truncated]' : resultText;\n block.innerHTML = renderMarkdown(displayText);\n section.appendChild(label);\n section.appendChild(block);\n fragment.appendChild(section);\n hasContent = true;\n }\n }\n }\n }\n\n if (hasContent) {\n els.detailPretty.appendChild(fragment);\n els.detailPretty.hidden = false;\n } else {\n els.detailPretty.hidden = true;\n }\n }\n\n function openImageModal(src, altText = '') {\n if (!src) return;\n lastActiveElementBeforeModal = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n lastBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n\n els.imageModalImg.src = src;\n els.imageModalImg.alt = altText;\n els.imageModal.classList.add('open');\n els.imageModal.setAttribute('aria-hidden', 'false');\n els.imageModalClose.focus();\n }\n\n function closeImageModal() {\n if (!els.imageModal.classList.contains('open')) return;\n els.imageModal.classList.remove('open');\n els.imageModal.setAttribute('aria-hidden', 'true');\n els.imageModalImg.removeAttribute('src');\n els.imageModalImg.alt = '';\n document.body.style.overflow = lastBodyOverflow ?? '';\n\n if (lastActiveElementBeforeModal) {\n lastActiveElementBeforeModal.focus();\n }\n lastActiveElementBeforeModal = null;\n lastBodyOverflow = null;\n }\n\n function renderDetailImages(obj) {\n const urls = extractImageUrls(obj);\n els.detailImages.innerHTML = '';\n if (!urls.length) {\n els.detailImages.hidden = true;\n return;\n }\n const fragment = document.createDocumentFragment();\n urls.forEach((src, idx) => {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'thumb';\n btn.title = `Open image ${idx + 1}`;\n const img = document.createElement('img');\n img.loading = 'lazy';\n img.src = src;\n img.alt = `Image ${idx + 1}`;\n btn.appendChild(img);\n btn.addEventListener('click', () => openImageModal(src, `Image ${idx + 1}`));\n fragment.appendChild(btn);\n });\n els.detailImages.appendChild(fragment);\n els.detailImages.hidden = false;\n }\n\n function isEditableElement(el) {\n if (!(el instanceof HTMLElement)) return false;\n if (el.isContentEditable) return true;\n const tag = el.tagName;\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';\n }\n\n function moveSelection(delta) {\n if (!state.filtered.length) return;\n const idx = state.filtered.findIndex(e => e.id === state.selectedId);\n if (idx === -1) {\n selectEvent(state.filtered[0].id, { scroll: true });\n return;\n }\n const nextIdx = Math.max(0, Math.min(state.filtered.length - 1, idx + delta));\n if (nextIdx === idx) return;\n selectEvent(state.filtered[nextIdx].id, { scroll: true });\n }\n\n async function copyText(text) {\n if (typeof text !== 'string') return;\n try {\n await navigator.clipboard.writeText(text);\n setStatus('Copied to clipboard.', 'good');\n } catch (error) {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n document.body.removeChild(ta);\n setStatus('Copied to clipboard.', 'good');\n }\n }\n\n function extractUserPrompts() {\n const prompts = [];\n for (const e of state.events) {\n // Filter: type=user, content-type includes text, role=user\n if (e.kind !== 'user') continue;\n if (e.role !== 'user') continue;\n if (!e.contentTypes.includes('text')) continue;\n // Skip compact summary messages (auto-generated context summaries)\n if (e.obj?.isCompactSummary === true) continue;\n\n const message = e.obj?.message;\n if (!message) continue;\n\n const content = message.content;\n if (typeof content === 'string') {\n prompts.push(content);\n } else if (Array.isArray(content)) {\n for (const item of content) {\n if (item?.type === 'text' && item.text) {\n prompts.push(item.text);\n }\n }\n }\n }\n return prompts.join('\\n\\n');\n }\n\n function openPromptsModal(text) {\n lastActiveElementBeforePrompts = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n lastBodyOverflowBeforePrompts = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n\n els.promptsText.value = text || '';\n els.promptsModal.classList.add('open');\n els.promptsModal.setAttribute('aria-hidden', 'false');\n els.promptsClose.focus();\n els.promptsText.scrollTop = 0;\n }\n\n function closePromptsModal() {\n if (!els.promptsModal.classList.contains('open')) return;\n els.promptsModal.classList.remove('open');\n els.promptsModal.setAttribute('aria-hidden', 'true');\n els.promptsText.value = '';\n document.body.style.overflow = lastBodyOverflowBeforePrompts ?? '';\n\n if (lastActiveElementBeforePrompts) lastActiveElementBeforePrompts.focus();\n lastActiveElementBeforePrompts = null;\n lastBodyOverflowBeforePrompts = null;\n }\n\n function setTimezoneMode(mode) {\n state.tzMode = mode;\n els.tzLocal.classList.toggle('active', mode === 'local');\n els.tzUtc.classList.toggle('active', mode === 'utc');\n applyFilters();\n if (state.selectedId !== null) selectEvent(state.selectedId);\n syncUrlHashFromControls();\n }\n\n els.promptsClose.addEventListener('click', closePromptsModal);\n els.promptsModal.addEventListener('click', (event) => {\n if (event.target === els.promptsModal) closePromptsModal();\n });\n els.promptsCopy.addEventListener('click', async () => {\n const text = els.promptsText.value || '';\n if (!text.trim()) {\n setStatus('No user prompts found.', 'warn');\n return;\n }\n await copyText(text);\n if (promptsCopiedTimer) clearTimeout(promptsCopiedTimer);\n const originalText = els.promptsCopy.textContent;\n els.promptsCopy.textContent = 'Copied!';\n promptsCopiedTimer = setTimeout(() => {\n els.promptsCopy.textContent = originalText;\n promptsCopiedTimer = null;\n }, 1500);\n });\n els.showPrompts.addEventListener('click', () => {\n const text = extractUserPrompts();\n if (!text) {\n setStatus('No user prompts found.', 'warn');\n return;\n }\n openPromptsModal(text);\n });\n\n els.imageModalClose.addEventListener('click', closeImageModal);\n els.imageModal.addEventListener('click', (event) => {\n if (event.target === els.imageModal) closeImageModal();\n });\n document.addEventListener('keydown', (event) => {\n if (event.key !== 'Escape') return;\n if (els.promptsModal.classList.contains('open')) closePromptsModal();\n if (els.imageModal.classList.contains('open')) closeImageModal();\n });\n\n document.addEventListener('keydown', (event) => {\n if (els.imageModal.classList.contains('open')) return;\n if (els.promptsModal.classList.contains('open')) return;\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;\n if (isEditableElement(document.activeElement)) return;\n if (document.activeElement !== els.timeline && !els.timeline.contains(document.activeElement)) return;\n if (state.selectedId === null) return;\n event.preventDefault();\n moveSelection(event.key === 'ArrowUp' ? -1 : 1);\n });\n\n if (typeof wideLayoutMq.addEventListener === 'function') {\n wideLayoutMq.addEventListener('change', updateDetailPlacement);\n } else if (typeof wideLayoutMq.addListener === 'function') {\n wideLayoutMq.addListener(updateDetailPlacement);\n }\n\n function setTab(tabName) {\n for (const tab of document.querySelectorAll('.tab')) {\n const active = tab.dataset.tab === tabName;\n tab.classList.toggle('active', active);\n tab.setAttribute('aria-selected', active ? 'true' : 'false');\n }\n for (const panel of document.querySelectorAll('[data-panel]')) {\n panel.hidden = panel.dataset.panel !== tabName;\n }\n }\n\n function setShareUrlParam(url) {\n const current = new URL(window.location.href);\n if (url) current.searchParams.set('url', url);\n else current.searchParams.delete('url');\n window.history.replaceState(null, '', current);\n }\n\n async function fetchAndRenderUrl(url, { syncUrl = true } = {}) {\n const trimmed = (url || '').trim();\n if (!trimmed) {\n setStatus('Enter a URL first.', 'error');\n return;\n }\n if (syncUrl) setShareUrlParam(trimmed);\n setStatus(`Fetching ${trimmed}...`, 'warn');\n try {\n const resp = await fetch(trimmed, { mode: 'cors' });\n if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n const text = await resp.text();\n await loadText(text, trimmed);\n } catch (error) {\n setStatus(`Failed to fetch URL: ${String(error)}`, 'error');\n }\n }\n\n async function loadText(text, sourceLabel) {\n const started = performance.now();\n state.sourceLabel = sourceLabel || null;\n setStatus(`Parsing ${sourceLabel || 'content'}...`, 'warn');\n closePromptsModal();\n els.showPrompts.disabled = true;\n clearSelection({ syncHash: false });\n\n const lines = text.split(/\\r?\\n/);\n const events = [];\n const errors = [];\n let idCounter = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (!line.trim()) continue;\n const parsed = safeJsonParse(line);\n if (!parsed.ok) {\n errors.push({ line: i + 1, error: parsed.error });\n continue;\n }\n const obj = parsed.value;\n const ts = typeof obj.timestamp === 'string' ? new Date(obj.timestamp) : null;\n const kind = obj.type || '(unknown)';\n const message = obj.message && typeof obj.message === 'object' ? obj.message : null;\n const contentTypes = message ? getContentTypes(message) : [];\n const role = message?.role ?? null;\n\n const summary = summarizeEvent(obj);\n events.push({\n id: ++idCounter,\n line: i + 1,\n raw: line,\n obj,\n date: ts,\n kind,\n contentTypes,\n role,\n summary,\n search: buildSearchText(obj, summary),\n });\n }\n\n state.events = events;\n els.showPrompts.disabled = !events.length;\n updateFilterOptions(events);\n applyFilters();\n applySelectionFromHash();\n syncUrlHashFromControls();\n\n const durationMs = Math.round(performance.now() - started);\n if (errors.length) {\n const first = errors[0];\n setStatus(\n `Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.\\n` +\n `Could not parse ${errors.length} line(s). First error at line ${first.line}: ${String(first.error)}`,\n 'warn'\n );\n } else {\n setStatus(`Loaded ${events.length} events from ${sourceLabel || 'content'} in ${durationMs}ms.`, 'good');\n }\n }\n\n async function handleFile(file) {\n if (!file) return;\n setShareUrlParam(null);\n try {\n const text = await file.text();\n await loadText(text, file.name);\n } catch (error) {\n setStatus(`Failed to read file: ${String(error)}`, 'error');\n }\n }\n\n els.dropzone.addEventListener('click', () => els.fileInput.click());\n els.dropzone.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n els.fileInput.click();\n }\n });\n els.fileInput.addEventListener('change', () => handleFile(els.fileInput.files?.[0]));\n\n els.dropzone.addEventListener('dragover', (e) => {\n e.preventDefault();\n els.dropzone.classList.add('dragover');\n });\n els.dropzone.addEventListener('dragleave', () => els.dropzone.classList.remove('dragover'));\n els.dropzone.addEventListener('drop', (e) => {\n e.preventDefault();\n els.dropzone.classList.remove('dragover');\n const file = e.dataTransfer?.files?.[0];\n if (file) handleFile(file);\n });\n\n els.renderPaste.addEventListener('click', async () => {\n const text = els.pasteInput.value || '';\n if (!text.trim()) {\n setStatus('Paste some JSONL content first.', 'error');\n return;\n }\n setShareUrlParam(null);\n await loadText(text, 'pasted content');\n });\n\n els.fetchUrl.addEventListener('click', async () => {\n await fetchAndRenderUrl(els.urlInput.value, { syncUrl: true });\n });\n\n for (const tab of document.querySelectorAll('.tab')) {\n tab.addEventListener('click', () => setTab(tab.dataset.tab));\n }\n\n els.tzLocal.addEventListener('click', () => setTimezoneMode('local'));\n els.tzUtc.addEventListener('click', () => setTimezoneMode('utc'));\n\n els.search.addEventListener('input', () => {\n state.filters.query = els.search.value.trim().toLowerCase();\n applyFilters();\n syncUrlHashFromControls();\n });\n els.typeFilter.addEventListener('change', () => {\n state.filters.type = els.typeFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.contentTypeFilter.addEventListener('change', () => {\n state.filters.contentType = els.contentTypeFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.roleFilter.addEventListener('change', () => {\n state.filters.role = els.roleFilter.value;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.hideThinking.addEventListener('change', () => {\n state.filters.hideThinking = els.hideThinking.checked;\n applyFilters();\n syncUrlHashFromControls();\n });\n els.truncateStrings.addEventListener('change', () => {\n if (state.selectedId !== null) selectEvent(state.selectedId);\n syncUrlHashFromControls();\n });\n\n els.clearFilters.addEventListener('click', () => {\n els.search.value = '';\n state.filters.query = '';\n state.filters.type = 'all';\n state.filters.contentType = 'all';\n state.filters.role = 'all';\n state.filters.hideThinking = false;\n els.hideThinking.checked = false;\n updateFilterOptions(state.events);\n applyFilters();\n syncUrlHashFromControls();\n });\n\n els.copyJson.addEventListener('click', () => {\n if (!state.selected) return;\n copyText(stringifyJson(state.selected.obj, false));\n });\n els.copyRaw.addEventListener('click', () => {\n if (!state.selected) return;\n copyText(state.selected.raw);\n });\n\n async function initFromQuery() {\n const url = new URLSearchParams(window.location.search).get('url');\n if (!url) return;\n setTab('url');\n els.urlInput.value = url;\n await fetchAndRenderUrl(url, { syncUrl: false });\n }\n\n // Initialize select options for the empty state.\n applyControlsFromHash();\n updateFilterOptions([]);\n syncUrlHashFromControls();\n window.addEventListener('hashchange', () => {\n applyControlsFromHash();\n updateFilterOptions(state.events);\n applyFilters();\n applySelectionFromHash();\n });\n void initFromQuery();\n </script>\n</body>\n</html>\n","structuredPatch":[{"oldStart":2103,"oldLines":8,"newStart":2103,"newLines":9,"lines":[" if (e.kind !== 'user') continue;"," if (e.role !== 'user') continue;"," if (!e.contentTypes.includes('text')) continue;","- // Skip compact summary messages (auto-generated context summaries)","+ // Skip compact summary messages (auto-generated context summaries) and meta messages"," if (e.obj?.isCompactSummary === true) continue;","+ if (e.obj?.isMeta === true) continue;"," "," const message = e.obj?.message;"," if (!message) continue;"]}],"userModified":false,"replaceAll":false}}
{"parentUuid":"e29b0c9d-f838-4338-8c38-0d764b850dd5","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01Qmo5kx2LBdmG2wkS8jW2hv","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Done. Now the extractUserPrompts function skips both `isCompactSummary: true` and `isMeta: true` messages.","signature":"EpICCkYIChgCKkCHoL/6WQehgcrL/wXs4quUfHUQecmAe5yANoXJlezJEy9Gldlk0PQZQKKrKABL26cLLE0dqth20Vl1GvGrp8F+EgyGX5lgB6GQ3dUy1+kaDBl22R9lIK7W7FCD0SIwk6g6qZcJI24ZXqcvTj6fW3xnUEOeau/ZyV/s1ZlumVTHYfrXFdj/RiEz1OgNvru3KnrmSsvnBbv9S6WKrWJiLuxhIGr406+cJSo7bfIbVAc5xuV7B8s6DRHOskpKHKa8eyLnK6wm6YPOgnyYhJrwsm4V+O+aF97Cz9zcdq1HXvyKUGT0tqm+24nJ2CW3/TocxG0hOlAIG4tkUibdjLT8n7XgCRnmwuKz/WVynhgB"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":478,"cache_read_input_tokens":54494,"cache_creation":{"ephemeral_5m_input_tokens":478,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CWFR2NGh9ubu27MPq6qcb","type":"assistant","uuid":"5d496536-bae9-414b-a6b5-63daab17977d","timestamp":"2025-12-19T04:28:40.616Z"}
{"parentUuid":"5d496536-bae9-414b-a6b5-63daab17977d","isSidechain":false,"userType":"external","cwd":"/Users/simon/Dropbox/dev/tools","sessionId":"8f5e8399-da56-4001-8c1e-c790821c87d4","version":"2.0.73","gitBranch":"main","slug":"shiny-doodling-charm","message":{"model":"claude-opus-4-5-20251101","id":"msg_01Qmo5kx2LBdmG2wkS8jW2hv","type":"message","role":"assistant","content":[{"type":"text","text":"Done. The function now also skips prompts with `isMeta: true`."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":8,"cache_creation_input_tokens":478,"cache_read_input_tokens":54494,"cache_creation":{"ephemeral_5m_input_tokens":478,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CWFR2NGh9ubu27MPq6qcb","type":"assistant","uuid":"c686ef9a-f02f-49c9-be57-803f1f2aa01d","timestamp":"2025-12-19T04:28:40.877Z"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment