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>