Plain is headed towards 1.0! Subscribe for development updates →

  1<div class="px-6 py-4" data-exception-container>
  2    <div class="flex justify-between items-center mb-4">
  3        <div class="text-amber-400" 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">
  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-stone-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-stone-500/30 data-[category=app]:text-stone-200
 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-neutral-500/30 data-[category=third-party]:text-neutral-300"
 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        const fullText = `Exception: ${exceptionText}\n\n${traceback}`;
 91
 92        navigator.clipboard.writeText(fullText).then(() => {
 93            // Change icon and text to indicate success
 94            button.innerHTML = `
 95                <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">
 96                    <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"/>
 97                </svg>
 98                <span>Copied!</span>
 99            `;
100
101            // Reset after 2 seconds
102            setTimeout(() => {
103                button.innerHTML = `
104                    <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">
105                        <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"/>
106                        <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"/>
107                    </svg>
108                    <span>Copy</span>
109                `;
110            }, 2000);
111        }).catch(err => {
112            console.error('Failed to copy exception:', err);
113            alert('Failed to copy exception to clipboard');
114        });
115    }
116
117    // Set up copy event listener
118    const copyButton = document.querySelector('[data-copy-exception]');
119    if (copyButton) {
120        copyButton.addEventListener('click', function() {
121            copyException(this);
122        });
123    }
124
125    // Set up raw toggle event listener
126    const toggleButton = document.querySelector('[data-toggle-raw]');
127    if (toggleButton) {
128        toggleButton.addEventListener('click', function() {
129            const container = this.closest('[data-exception-container]');
130            const frames = container.querySelector('[data-exception-frames]');
131            const raw = container.querySelector('[data-exception-traceback]');
132
133            frames.classList.toggle('hidden');
134            raw.classList.toggle('hidden');
135
136            // Update button text
137            const isRawVisible = !raw.classList.contains('hidden');
138            this.querySelector('span').textContent = isRawVisible ? 'View frames' : 'View raw';
139        });
140    }
141
142    // Set up frame toggle event listeners
143    document.querySelectorAll('[data-toggle-frame]').forEach(btn => {
144        btn.addEventListener('click', function() {
145            const frame = this.closest('[data-expanded]');
146            const content = frame.querySelector('[data-frame-content]');
147            const isExpanded = frame.dataset.expanded === 'true';
148
149            frame.dataset.expanded = isExpanded ? 'false' : 'true';
150            content.dataset.visible = isExpanded ? 'false' : 'true';
151        });
152    });
153
154    // Prevent VS Code links from toggling frame
155    document.querySelectorAll('[data-vscode-link]').forEach(link => {
156        link.addEventListener('click', function(e) {
157            e.stopPropagation();
158        });
159    });
160})();
161</script>