Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ jobs:
- name: Install dependencies
run: |
pip install -e . --group dev
- name: Cache Playwright browsers
uses: actions/cache@v5
with:
path: |
~/.cache/ms-playwright/
~/Library/Caches/ms-playwright/
~\AppData\Local\ms-playwright\
key: ${{ runner.os }}-playwright
- name: Install Playwright browsers
run: |
python -m playwright install chromium
- name: Run tests
env:
PYTHONUTF8: "1"
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ dev = [
"pytest>=9.0.2",
"pytest-httpx>=0.35.0",
"syrupy>=5.0.0",
"pytest-playwright>=0.7.0",
]
263 changes: 78 additions & 185 deletions src/claude_code_transcripts/__init__.py

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions src/claude_code_transcripts/templates/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@
{%- endmacro %}

{# Message wrapper - content_html is pre-rendered so needs |safe #}
{% macro message(role_class, role_label, msg_id, timestamp, content_html) %}
<div class="message {{ role_class }}" id="{{ msg_id }}"><div class="message-header"><span class="role-label">{{ role_label }}</span><a href="#{{ msg_id }}" class="timestamp-link"><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></a></div><div class="message-content">{{ content_html|safe }}</div></div>
{% macro message(role_class, role_label, msg_id, timestamp, content_html, copy_label='Copy Markdown', copy_content=None, copy_from=None) %}
<div class="message {{ role_class }}" id="{{ msg_id }}"><div class="message-header"><span class="role-label">{{ role_label }}</span><span class="header-actions">{% if copy_content %}<copy-button label="{{ copy_label }}" data-content="{{ copy_content|e }}"></copy-button>{% elif copy_from %}<copy-button label="{{ copy_label }}" data-content-from="{{ copy_from }}"></copy-button>{% endif %}<a href="#{{ msg_id }}" class="timestamp-link"><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></a></span></div><div class="message-content">{{ content_html|safe }}</div></div>
{%- endmacro %}

{# Continuation wrapper - content_html is pre-rendered so needs |safe #}
Expand All @@ -157,17 +157,17 @@
{%- endmacro %}

{# Index item (prompt) - rendered_content and stats_html are pre-rendered so need |safe #}
{% macro index_item(prompt_num, link, timestamp, rendered_content, stats_html) %}
<div class="index-item"><a href="{{ link }}"><div class="index-item-header"><span class="index-item-number">#{{ prompt_num }}</span><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></div><div class="index-item-content">{{ rendered_content|safe }}</div></a>{{ stats_html|safe }}</div>
{% macro index_item(prompt_num, link, timestamp, rendered_content, stats_html, copy_content=None) %}
<div class="index-item"><a href="{{ link }}"><div class="index-item-header"><span class="index-item-number">#{{ prompt_num }}</span><span class="header-actions">{% if copy_content %}<copy-button label="Copy Markdown" data-content="{{ copy_content|e }}"></copy-button>{% endif %}<time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></span></div><div class="index-item-content">{{ rendered_content|safe }}</div></a>{{ stats_html|safe }}</div>
{%- endmacro %}

{# Index commit #}
{% macro index_commit(commit_hash, commit_msg, timestamp, github_repo) %}
{%- if github_repo -%}
{%- set github_link = 'https://github.com/' ~ github_repo ~ '/commit/' ~ commit_hash -%}
<div class="index-commit"><a href="{{ github_link }}"><div class="index-commit-header"><span class="index-commit-hash">{{ commit_hash[:7] }}</span><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></div><div class="index-commit-msg">{{ commit_msg }}</div></a></div>
<div class="index-commit"><a href="{{ github_link }}"><div class="index-commit-header"><span class="index-commit-hash">{{ commit_hash[:7] }}</span><span class="header-actions"><copy-button label="Copy text" data-content-from=".index-commit-msg"></copy-button><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></span></div><div class="index-commit-msg">{{ commit_msg }}</div></a></div>
{%- else -%}
<div class="index-commit"><div class="index-commit-header"><span class="index-commit-hash">{{ commit_hash[:7] }}</span><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></div><div class="index-commit-msg">{{ commit_msg }}</div></div>
<div class="index-commit"><div class="index-commit-header"><span class="index-commit-hash">{{ commit_hash[:7] }}</span><span class="header-actions"><copy-button label="Copy text" data-content-from=".index-commit-msg"></copy-button><time datetime="{{ timestamp }}" data-timestamp="{{ timestamp }}">{{ timestamp }}</time></span></div><div class="index-commit-msg">{{ commit_msg }}</div></div>
{%- endif %}
{%- endmacro %}

Expand Down
64 changes: 64 additions & 0 deletions src/claude_code_transcripts/templates/scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
class CopyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const label = this.getAttribute('label') || 'Copy';
const style = document.createElement('style');
style.textContent = 'button { background: transparent; border: 1px solid #757575; border-radius: 4px; padding: 2px 6px; font-size: 0.7rem; color: #757575; cursor: pointer; white-space: nowrap; font-family: inherit; } button:hover { background: rgba(0,0,0,0.05); border-color: #1976d2; color: #1976d2; } button.copied { background: #e8f5e9; border-color: #4caf50; color: #2e7d32; }';
const btn = document.createElement('button');
btn.textContent = label;
btn.addEventListener('click', (e) => this.handleClick(e, btn, label));
this.shadowRoot.appendChild(style);
this.shadowRoot.appendChild(btn);
}
handleClick(e, btn, label) {
e.preventDefault();
e.stopPropagation();
let content = this.getAttribute('data-content');
if (!content) {
const selector = this.getAttribute('data-content-from');
if (selector) {
const el = this.closest('.message, .index-item, .index-commit, .search-result')?.querySelector(selector) || document.querySelector(selector);
if (el) content = el.innerText;
}
}
if (content) {
navigator.clipboard.writeText(content).then(() => {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = label; btn.classList.remove('copied'); }, 2000);
});
}
}
}
customElements.define('copy-button', CopyButton);
document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
const timestamp = el.getAttribute('data-timestamp');
const date = new Date(timestamp);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
if (isToday) { el.textContent = timeStr; }
else { el.textContent = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + timeStr; }
});
document.querySelectorAll('pre.json').forEach(function(el) {
let text = el.textContent;
text = text.replace(/"([^"]+)":/g, '<span style="color: #ce93d8">"$1"</span>:');
text = text.replace(/: "([^"]*)"/g, ': <span style="color: #81d4fa">"$1"</span>');
text = text.replace(/: (\d+)/g, ': <span style="color: #ffcc80">$1</span>');
text = text.replace(/: (true|false|null)/g, ': <span style="color: #f48fb1">$1</span>');
el.innerHTML = text;
});
document.querySelectorAll('.truncatable').forEach(function(wrapper) {
const content = wrapper.querySelector('.truncatable-content');
const btn = wrapper.querySelector('.expand-btn');
if (content.scrollHeight > 250) {
wrapper.classList.add('truncated');
btn.addEventListener('click', function() {
if (wrapper.classList.contains('truncated')) { wrapper.classList.remove('truncated'); wrapper.classList.add('expanded'); btn.textContent = 'Show less'; }
else { wrapper.classList.remove('expanded'); wrapper.classList.add('truncated'); btn.textContent = 'Show more'; }
});
}
});
7 changes: 5 additions & 2 deletions src/claude_code_transcripts/templates/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@

var resultDiv = document.createElement('div');
resultDiv.className = 'search-result';
resultDiv.innerHTML = '<a href="' + link + '">' +
'<div class="search-result-page">' + escapeHtml(pageFile) + '</div>' +
resultDiv.innerHTML = '<div class="search-result-header">' +
'<copy-button label="Copy text" data-content-from=".search-result-content"></copy-button>' +
'<a href="' + link + '" class="search-result-page">' + escapeHtml(pageFile) + '</a>' +
'</div>' +
'<a href="' + link + '">' +
'<div class="search-result-content">' + clone.innerHTML + '</div>' +
'</a>';
searchResults.appendChild(resultDiv);
Expand Down
Loading
Loading