v0.151.0
  1<div class="px-6 py-4" data-exception-container>
  2    <div class="flex justify-between items-start mb-4">
  3        <div class="text-amber-400 min-w-0 break-words" data-exception-message>
  4            <span class="font-semibold">{{ exception_context.exception.__class__.__name__ }}</span>: {{ exception_context.exception }}
  5        </div>
  6        <div class="flex items-center space-x-2 ml-4 shrink-0">
  7            <button data-toggle-raw class="flex items-center space-x-1 px-2 py-0.5 text-sm text-stone-300 rounded bg-white/10 hover:bg-white/20 transition-colors cursor-pointer">
  8                <span>View raw</span>
  9            </button>
 10            <button data-copy-exception class="flex items-center space-x-1 px-2 py-0.5 text-sm text-stone-300 rounded bg-white/10 hover:bg-white/20 transition-colors cursor-pointer">
 11                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3.5 h-3.5 bi bi-clipboard" viewBox="0 0 16 16">
 12                    <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
 13                    <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
 14                </svg>
 15                <span>Copy</span>
 16            </button>
 17        </div>
 18    </div>
 19
 20    {# Traceback frames with source code #}
 21    <div class="space-y-1" data-exception-frames>
 22        {% for frame in exception_context.traceback_frames %}
 23        <div
 24            data-category="{{ frame.category }}"
 25            data-expanded="{{ 'true' if frame.category == 'app' else 'false' }}"
 26            class="group rounded overflow-hidden border border-white/10 bg-white/5 data-[category=app]:bg-amber-500/10"
 27        >
 28            <button
 29                data-toggle-frame
 30                class="w-full px-2 py-1 text-xs flex justify-between items-center text-stone-400 hover:text-stone-300 hover:bg-white/5 cursor-pointer group-data-[category=app]:text-stone-300"
 31            >
 32                <div class="flex items-center gap-2">
 33                    <svg data-frame-chevron class="w-3 h-3 transition-transform group-data-[expanded=false]:-rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 34                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
 35                    </svg>
 36                    <span data-category="{{ frame.category }}" class="px-1.5 py-0.5 rounded text-[10px] font-medium
 37                        data-[category=app]:bg-amber-500/30 data-[category=app]:text-amber-300
 38                        data-[category=plain]:bg-emerald-500/30 data-[category=plain]:text-emerald-300
 39                        data-[category=plainx]:bg-violet-500/30 data-[category=plainx]:text-violet-300
 40                        data-[category=python]:bg-sky-500/30 data-[category=python]:text-sky-300
 41                        data-[category=third-party]:bg-zinc-600/30 data-[category=third-party]:text-zinc-400"
 42                    >{{ frame.category }}</span>
 43                    <a href="vscode://file/{{ frame.filename }}:{{ frame.lineno }}" data-vscode-link class="font-mono hover:underline text-stone-500">{{ frame.filename }}</a>
 44                </div>
 45                <span>line {{ frame.lineno }} in <span class="font-semibold">{{ frame.name }}</span></span>
 46            </button>
 47            <div data-frame-content data-visible="{{ 'true' if frame.category == 'app' else 'false' }}" class="data-[visible=false]:hidden">
 48                {% if frame.source_lines %}
 49                <div class="text-xs overflow-auto">
 50                    <table class="w-full">
 51                        {% for line in frame.source_lines %}
 52                        <tr data-error-line="{{ line.is_error_line|lower }}" class="data-[error-line=true]:bg-amber-500/20">
 53                            <td class="px-2 py-0.5 text-right text-stone-500 select-none w-12 font-mono">{{ line.lineno }}</td>
 54                            <td class="px-2 py-0.5 font-mono whitespace-pre text-stone-300">{{ line.code }}</td>
 55                        </tr>
 56                        {% endfor %}
 57                    </table>
 58                </div>
 59                {% endif %}
 60                {% if frame.locals %}
 61                <div class="border-t border-white/10">
 62                    <table class="text-xs">
 63                        {% for var in frame.locals %}
 64                        <tr class="hover:bg-white/5">
 65                            <td class="py-0.5 pl-2 pr-4 text-stone-400 font-mono whitespace-nowrap">{{ var.name }}</td>
 66                            <td class="py-0.5 pr-4 text-stone-500 whitespace-nowrap">{{ var.type }}</td>
 67                            <td class="py-0.5 pr-2 text-stone-400 font-mono">{{ var.value }}</td>
 68                        </tr>
 69                        {% endfor %}
 70                    </table>
 71                </div>
 72                {% endif %}
 73            </div>
 74        </div>
 75        {% endfor %}
 76    </div>
 77
 78    {# Raw traceback (hidden by default, toggled with Raw button) #}
 79    <div class="hidden mt-4 text-stone-300 text-xs bg-white/5 p-2 rounded overflow-auto" data-exception-traceback>
 80        <pre><code>{{ exception_context.traceback_string }}</code></pre>
 81    </div>
 82</div>
 83
 84<script nonce="{{ request.csp_nonce }}">
 85(function() {
 86    function copyException(button) {
 87        const container = button.closest('[data-exception-container]');
 88        const exceptionText = container.querySelector('[data-exception-message]').textContent.trim();
 89        const traceback = container.querySelector('[data-exception-traceback] code').textContent;
 90        // Match Python's stdlib traceback format. The first line must NOT look like `word:` —
 91        // iOS data detectors will treat it as a URL scheme and percent-encode the pasteboard.
 92        const fullText = `Traceback (most recent call last):\n${traceback}${exceptionText}\n`;
 93
 94        navigator.clipboard.writeText(fullText).then(() => {
 95            // Change icon and text to indicate success
 96            button.innerHTML = `
 97                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3.5 h-3.5 bi bi-check2" viewBox="0 0 16 16">
 98                    <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
 99                </svg>
100                <span>Copied!</span>
101            `;
102
103            // Reset after 2 seconds
104            setTimeout(() => {
105                button.innerHTML = `
106                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3.5 h-3.5 bi bi-clipboard" viewBox="0 0 16 16">
107                        <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
108                        <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
109                    </svg>
110                    <span>Copy</span>
111                `;
112            }, 2000);
113        }).catch(err => {
114            console.error('Failed to copy exception:', err);
115            alert('Failed to copy exception to clipboard');
116        });
117    }
118
119    // Set up copy event listener
120    const copyButton = document.querySelector('[data-copy-exception]');
121    if (copyButton) {
122        copyButton.addEventListener('click', function() {
123            copyException(this);
124        });
125    }
126
127    // Set up raw toggle event listener
128    const toggleButton = document.querySelector('[data-toggle-raw]');
129    if (toggleButton) {
130        toggleButton.addEventListener('click', function() {
131            const container = this.closest('[data-exception-container]');
132            const frames = container.querySelector('[data-exception-frames]');
133            const raw = container.querySelector('[data-exception-traceback]');
134
135            frames.classList.toggle('hidden');
136            raw.classList.toggle('hidden');
137
138            // Update button text
139            const isRawVisible = !raw.classList.contains('hidden');
140            this.querySelector('span').textContent = isRawVisible ? 'View frames' : 'View raw';
141        });
142    }
143
144    // Set up frame toggle event listeners
145    document.querySelectorAll('[data-toggle-frame]').forEach(btn => {
146        btn.addEventListener('click', function() {
147            const frame = this.closest('[data-expanded]');
148            const content = frame.querySelector('[data-frame-content]');
149            const isExpanded = frame.dataset.expanded === 'true';
150
151            frame.dataset.expanded = isExpanded ? 'false' : 'true';
152            content.dataset.visible = isExpanded ? 'false' : 'true';
153        });
154    });
155
156    // Prevent VS Code links from toggling frame
157    document.querySelectorAll('[data-vscode-link]').forEach(link => {
158        link.addEventListener('click', function(e) {
159            e.stopPropagation();
160        });
161    });
162})();
163</script>