Default Model column to original; configurable in Options (v1.9.14)

- Backup filename prefix: claude-exporter-backup-* → claude-database-*
  (same timestamp format, still JSON)
- Browse Model column display is now controlled by a new modelDisplay
  preference saved in chrome.storage.local. Default is 'original'
  (revert of v1.9.12's flip-to-current), with 'current' available as
  an opt-in via a new "Model Display" section on the Options page.
- Bounced chats keep the "*" marker in either mode. Tooltip text
  adapts: "Originally X" when displaying current, "Now using X" when
  displaying original. Bounce signal is preserved regardless of which
  model the cell shows.
- getDisplayModel now returns { model, other, otherLabel, bounced }
  with the displayed value chosen by the preference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
tux tucker 2026-05-18 18:22:34 -04:00
parent 3ff9b89ee2
commit 51ceebb71d
12 changed files with 181 additions and 23 deletions

View file

@ -51,6 +51,7 @@ let modelSnapshots = {}; // Map conversation UUID to { firstSeen, current, ... }
let statusFilter = 'all'; // 'all', 'new', 'exported'
let dateFormat = 'mdy'; // 'mdy' or 'dmy'
let timeFormat = '12h'; // '12h' or '24h'
let modelDisplay = 'original'; // 'original' (first-seen) or 'current'
// Export timestamp storage helpers
async function loadExportTimestamps() {
@ -74,20 +75,25 @@ async function loadModelSnapshots() {
});
}
// Resolve which model to show for a conversation: the current model from the
// snapshot if we have one, otherwise the conversation's reported/inferred
// model. Also reports the original (first-seen) model and whether the chat
// has since been bounced to a different one — bounced chats get a `*` marker.
// Resolve which model to show for a conversation. Honors the modelDisplay
// preference ('original' default, or 'current'). When the chat has been
// bounced (current differs from first-seen), `bounced` is true and the
// `*` marker shows the "other" model in its tooltip.
function getDisplayModel(conv) {
const snap = modelSnapshots[conv.uuid];
if (snap && snap.firstSeen) {
const original = snap.firstSeen;
const current = snap.current || snap.firstSeen;
const bounced = !!snap.current && snap.current !== snap.firstSeen;
const useCurrent = modelDisplay === 'current';
return {
model: snap.current || snap.firstSeen,
original: snap.firstSeen,
bounced: !!snap.current && snap.current !== snap.firstSeen
model: useCurrent ? current : original,
other: useCurrent ? original : current,
otherLabel: useCurrent ? 'Originally' : 'Now using',
bounced
};
}
return { model: conv.model, original: conv.model, bounced: false };
return { model: conv.model, other: conv.model, otherLabel: '', bounced: false };
}
async function saveExportTimestamp(conversationId) {
@ -117,6 +123,15 @@ async function loadDateTimePrefs() {
});
}
async function loadModelDisplayPref() {
return new Promise((resolve) => {
chrome.storage.local.get(['modelDisplay'], (result) => {
modelDisplay = result.modelDisplay === 'current' ? 'current' : 'original';
resolve();
});
});
}
function formatDate(dt) {
const m = dt.getMonth() + 1;
const d = dt.getDate();
@ -145,6 +160,7 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadExportTimestamps();
await loadModelSnapshots();
await loadDateTimePrefs();
await loadModelDisplayPref();
const elapsed = Date.now() - loadingStart;
if (elapsed < 1000) await new Promise(r => setTimeout(r, 1000 - elapsed));
const loadingText = document.getElementById('loadingText');
@ -444,7 +460,7 @@ function displayConversations() {
<td>
<span class="model-badge ${modelBadgeClass}">
${escapeHtml(formatModelName(modelInfo.model))}
</span>${modelInfo.bounced ? `<span class="model-bounced" title="Originally ${escapeHtml(formatModelName(modelInfo.original))}">*</span>` : ''}
</span>${modelInfo.bounced ? `<span class="model-bounced" title="${modelInfo.otherLabel} ${escapeHtml(formatModelName(modelInfo.other))}">*</span>` : ''}
</td>
<td>
<div class="actions">

View file

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Claude Exporter Beta",
"version": "1.9.13",
"version": "1.9.14",
"description": "Export conversations and artifacts from Claude.ai",
"permissions": [
"activeTab",

View file

@ -123,6 +123,32 @@
.date-time-grid label {
margin-bottom: 5px;
}
.radio-option {
display: block;
padding: 10px 12px;
border-radius: 5px;
margin-top: 8px;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--bg-body);
font-weight: normal;
}
.radio-option:hover {
border-color: var(--primary-color);
}
.radio-option input {
margin-right: 8px;
vertical-align: middle;
}
.radio-option strong {
font-weight: 600;
}
.radio-option .radio-desc {
display: block;
margin: 4px 0 0 22px;
font-size: 12px;
color: var(--text-secondary);
}
.status {
margin-top: 10px;
padding: 10px;
@ -235,6 +261,22 @@
<div id="dateTimeStatus" class="status"></div>
</div>
<div class="section">
<h3>Model Display</h3>
<p>Which model name to show in the browse view's Model column. Bounced chats (where the model has changed) get a small "*" marker either way — the tooltip shows the other model.</p>
<label class="radio-option">
<input type="radio" name="modelDisplay" value="original">
<strong>Original</strong>
<span class="radio-desc">The model that first ran the conversation (first-seen). Preserves the original even after the chat gets bounced to a newer model.</span>
</label>
<label class="radio-option">
<input type="radio" name="modelDisplay" value="current">
<strong>Current</strong>
<span class="radio-desc">The model the conversation uses right now. Reflects what will run if you reopen the chat.</span>
</label>
<div id="modelDisplayStatus" class="status"></div>
</div>
<script src="utils.js"></script>
<script src="options.js"></script>
</body>

View file

@ -106,6 +106,24 @@ document.getElementById('timeFormatSelect').addEventListener('change', (e) => {
});
});
// Model display preference (browse view's Model column)
function loadModelDisplayPref() {
chrome.storage.local.get(['modelDisplay'], (result) => {
const value = result.modelDisplay === 'current' ? 'current' : 'original';
const radio = document.querySelector(`input[name="modelDisplay"][value="${value}"]`);
if (radio) radio.checked = true;
});
}
loadModelDisplayPref();
document.querySelectorAll('input[name="modelDisplay"]').forEach((radio) => {
radio.addEventListener('change', (e) => {
chrome.storage.local.set({ modelDisplay: e.target.value }, () => {
showStatus('modelDisplayStatus', 'Model display preference saved. Reload the browse page to see the change.', 'success');
});
});
});
// Helper functions
function showStatus(elementId, message, type) {
const statusEl = document.getElementById(elementId);

View file

@ -694,7 +694,7 @@ function backupExtensionData(onComplete) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
a.download = `claude-database-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

View file

@ -1,5 +1,11 @@
# Changelog
## [1.9.14]
- Backup filename changed from `claude-exporter-backup-(timestamp).json` to `claude-database-(timestamp).json`
- Browse table Model column display is now configurable. Defaults to **Original** (first-seen model, the v1.9.4 behavior); a new "Model Display" section in Options lets you switch to **Current** (v1.9.12 behavior)
- Bounced chats keep the `*` marker in either mode; the tooltip now shows "Originally X" when displaying current, and "Now using X" when displaying original
## [1.9.13]
- Backup/Restore renamed throughout to **Export Backup** / **Import Backup** (options page buttons + browse dropdown items)

View file

@ -25,7 +25,7 @@
- **Track model changes per conversation**
- **Phase 1 capture SHIPPED (v1.9.3)**`recordModelSnapshots()` in `content.js` writes `modelSnapshots` to `chrome.storage.local` every time the conversation list is fetched (browse page load or popup "Export All"), not just on export. Stores `{firstSeen, firstSeenAt, current, currentAt, history[]}` per conversation UUID; raw API model only, never an inferred guess.
- **Browse-table display SHIPPED (v1.9.4, revised v1.9.12)** — Model column shows the **current** model via `getDisplayModel()`, with a `*` asterisk marker + "Originally X" tooltip when the chat has been bounced. Falls back to current/inferred when no snapshot exists. (v1.9.4 originally showed first-seen with a `→` marker; v1.9.12 flipped to current so the cell reflects what model the chat will actually use on reopen.)
- **Browse-table display SHIPPED (v1.9.4, revised v1.9.12, configurable v1.9.14)** — Model column shows either the original (first-seen) or current model via `getDisplayModel()`, controlled by the `modelDisplay` preference (default 'original'). Bounced chats get a `*` marker with a tooltip showing the "other" model ("Originally X" when displaying current, "Now using X" when displaying original). Options page "Model Display" section lets users switch.
- Still pending: surface the snapshot in JSON exports (sidecar or inline field); optionally a dedicated "current model" column or filter for bounced chats
- `conversation.model` from the API is the *current* model only — when chats get bounced (deprecation, guardrails kicking to Sonnet 4, etc.) the original model is lost
- Symptom: chats created before Sonnet 4.5 existed now show "Sonnet 4.5" because that's their current default

View file

@ -51,6 +51,7 @@ let modelSnapshots = {}; // Map conversation UUID to { firstSeen, current, ... }
let statusFilter = 'all'; // 'all', 'new', 'exported'
let dateFormat = 'mdy'; // 'mdy' or 'dmy'
let timeFormat = '12h'; // '12h' or '24h'
let modelDisplay = 'original'; // 'original' (first-seen) or 'current'
// Export timestamp storage helpers
async function loadExportTimestamps() {
@ -74,20 +75,25 @@ async function loadModelSnapshots() {
});
}
// Resolve which model to show for a conversation: the current model from the
// snapshot if we have one, otherwise the conversation's reported/inferred
// model. Also reports the original (first-seen) model and whether the chat
// has since been bounced to a different one — bounced chats get a `*` marker.
// Resolve which model to show for a conversation. Honors the modelDisplay
// preference ('original' default, or 'current'). When the chat has been
// bounced (current differs from first-seen), `bounced` is true and the
// `*` marker shows the "other" model in its tooltip.
function getDisplayModel(conv) {
const snap = modelSnapshots[conv.uuid];
if (snap && snap.firstSeen) {
const original = snap.firstSeen;
const current = snap.current || snap.firstSeen;
const bounced = !!snap.current && snap.current !== snap.firstSeen;
const useCurrent = modelDisplay === 'current';
return {
model: snap.current || snap.firstSeen,
original: snap.firstSeen,
bounced: !!snap.current && snap.current !== snap.firstSeen
model: useCurrent ? current : original,
other: useCurrent ? original : current,
otherLabel: useCurrent ? 'Originally' : 'Now using',
bounced
};
}
return { model: conv.model, original: conv.model, bounced: false };
return { model: conv.model, other: conv.model, otherLabel: '', bounced: false };
}
async function saveExportTimestamp(conversationId) {
@ -117,6 +123,15 @@ async function loadDateTimePrefs() {
});
}
async function loadModelDisplayPref() {
return new Promise((resolve) => {
chrome.storage.local.get(['modelDisplay'], (result) => {
modelDisplay = result.modelDisplay === 'current' ? 'current' : 'original';
resolve();
});
});
}
function formatDate(dt) {
const m = dt.getMonth() + 1;
const d = dt.getDate();
@ -145,6 +160,7 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadExportTimestamps();
await loadModelSnapshots();
await loadDateTimePrefs();
await loadModelDisplayPref();
const elapsed = Date.now() - loadingStart;
if (elapsed < 1000) await new Promise(r => setTimeout(r, 1000 - elapsed));
const loadingText = document.getElementById('loadingText');
@ -444,7 +460,7 @@ function displayConversations() {
<td>
<span class="model-badge ${modelBadgeClass}">
${escapeHtml(formatModelName(modelInfo.model))}
</span>${modelInfo.bounced ? `<span class="model-bounced" title="Originally ${escapeHtml(formatModelName(modelInfo.original))}">*</span>` : ''}
</span>${modelInfo.bounced ? `<span class="model-bounced" title="${modelInfo.otherLabel} ${escapeHtml(formatModelName(modelInfo.other))}">*</span>` : ''}
</td>
<td>
<div class="actions">

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Claude Exporter Beta",
"version": "1.9.13",
"version": "1.9.14",
"description": "Export conversations and artifacts from Claude.ai",
"permissions": [
"activeTab",

View file

@ -123,6 +123,32 @@
.date-time-grid label {
margin-bottom: 5px;
}
.radio-option {
display: block;
padding: 10px 12px;
border-radius: 5px;
margin-top: 8px;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--bg-body);
font-weight: normal;
}
.radio-option:hover {
border-color: var(--primary-color);
}
.radio-option input {
margin-right: 8px;
vertical-align: middle;
}
.radio-option strong {
font-weight: 600;
}
.radio-option .radio-desc {
display: block;
margin: 4px 0 0 22px;
font-size: 12px;
color: var(--text-secondary);
}
.status {
margin-top: 10px;
padding: 10px;
@ -235,6 +261,22 @@
<div id="dateTimeStatus" class="status"></div>
</div>
<div class="section">
<h3>Model Display</h3>
<p>Which model name to show in the browse view's Model column. Bounced chats (where the model has changed) get a small "*" marker either way — the tooltip shows the other model.</p>
<label class="radio-option">
<input type="radio" name="modelDisplay" value="original">
<strong>Original</strong>
<span class="radio-desc">The model that first ran the conversation (first-seen). Preserves the original even after the chat gets bounced to a newer model.</span>
</label>
<label class="radio-option">
<input type="radio" name="modelDisplay" value="current">
<strong>Current</strong>
<span class="radio-desc">The model the conversation uses right now. Reflects what will run if you reopen the chat.</span>
</label>
<div id="modelDisplayStatus" class="status"></div>
</div>
<script src="utils.js"></script>
<script src="options.js"></script>
</body>

View file

@ -106,6 +106,24 @@ document.getElementById('timeFormatSelect').addEventListener('change', (e) => {
});
});
// Model display preference (browse view's Model column)
function loadModelDisplayPref() {
chrome.storage.local.get(['modelDisplay'], (result) => {
const value = result.modelDisplay === 'current' ? 'current' : 'original';
const radio = document.querySelector(`input[name="modelDisplay"][value="${value}"]`);
if (radio) radio.checked = true;
});
}
loadModelDisplayPref();
document.querySelectorAll('input[name="modelDisplay"]').forEach((radio) => {
radio.addEventListener('change', (e) => {
chrome.storage.local.set({ modelDisplay: e.target.value }, () => {
showStatus('modelDisplayStatus', 'Model display preference saved. Reload the browse page to see the change.', 'success');
});
});
});
// Helper functions
function showStatus(elementId, message, type) {
const statusEl = document.getElementById(elementId);

View file

@ -694,7 +694,7 @@ function backupExtensionData(onComplete) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
a.download = `claude-database-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);