feat: v1.2.5 — UI improvements and session management fixes
- Fix session delete to scan all claude project dirs (not just first match) - Batch async rendering for message history with stale render guard - Add custom draggable scrollbar for chat area - Fix AskUserQuestion card rendered at bottom instead of top - Fix bubble split (msg-text + msg-tools) to prevent tool UI overwrite - Add delete confirmation dialog with warm theme styling - Support multiline display in user messages - Apply model config to settings.json immediately on save
This commit is contained in:
180
public/app.js
180
public/app.js
@@ -47,6 +47,7 @@
|
||||
let currentMode = localStorage.getItem('cc-web-mode') || 'yolo';
|
||||
let currentModel = 'opus';
|
||||
let loginPasswordValue = ''; // store login password for force-change flow
|
||||
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
|
||||
|
||||
// --- DOM ---
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
@@ -324,6 +325,16 @@
|
||||
|
||||
const msgEl = createMsgElement('assistant', '');
|
||||
msgEl.id = 'streaming-msg';
|
||||
// 流式消息 bubble 拆为 .msg-text 和 .msg-tools 两个子容器
|
||||
const bubble = msgEl.querySelector('.msg-bubble');
|
||||
bubble.innerHTML = '';
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'msg-text';
|
||||
textDiv.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
const toolsDiv = document.createElement('div');
|
||||
toolsDiv.className = 'msg-tools';
|
||||
bubble.appendChild(textDiv);
|
||||
bubble.appendChild(toolsDiv);
|
||||
messagesDiv.appendChild(msgEl);
|
||||
scrollToBottom();
|
||||
}
|
||||
@@ -361,7 +372,9 @@
|
||||
if (!streamEl) return;
|
||||
const bubble = streamEl.querySelector('.msg-bubble');
|
||||
if (!bubble) return;
|
||||
bubble.innerHTML = renderMarkdown(pendingText);
|
||||
let textDiv = bubble.querySelector('.msg-text');
|
||||
if (!textDiv) { textDiv = bubble; }
|
||||
textDiv.innerHTML = renderMarkdown(pendingText);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -391,6 +404,7 @@
|
||||
bubble.className = 'msg-bubble';
|
||||
|
||||
if (role === 'user') {
|
||||
bubble.style.whiteSpace = 'pre-wrap';
|
||||
bubble.textContent = content;
|
||||
} else {
|
||||
bubble.innerHTML = content ? renderMarkdown(content) : '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
@@ -401,13 +415,9 @@
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderMessages(messages) {
|
||||
messagesDiv.innerHTML = '';
|
||||
if (messages.length === 0) {
|
||||
messagesDiv.innerHTML = '<div class="welcome-msg"><div class="welcome-icon">✿</div><h3>欢迎使用 CC-Web</h3><p>开始与 Claude Code 对话</p></div>';
|
||||
return;
|
||||
}
|
||||
for (const m of messages) {
|
||||
let renderEpoch = 0;
|
||||
|
||||
function buildMsgElement(m) {
|
||||
const el = createMsgElement(m.role, m.content);
|
||||
if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
|
||||
const bubble = el.querySelector('.msg-bubble');
|
||||
@@ -416,20 +426,58 @@
|
||||
details.className = 'tool-call';
|
||||
details.dataset.toolName = tc.name || '';
|
||||
if (tc.name === 'AskUserQuestion') details.open = true;
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.innerHTML = `<span class="tool-call-icon done"></span> ${escapeHtml(tc.name)}`;
|
||||
details.appendChild(summary);
|
||||
|
||||
const displayInput = tc.name === 'AskUserQuestion' ? tc.input : (tc.result || tc.input);
|
||||
details.appendChild(buildToolContentElement(tc.name, displayInput));
|
||||
bubble.appendChild(details);
|
||||
}
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
bubble.insertBefore(details, bubble.firstChild);
|
||||
function renderMessages(messages) {
|
||||
renderEpoch++;
|
||||
const epoch = renderEpoch;
|
||||
messagesDiv.innerHTML = '';
|
||||
if (messages.length === 0) {
|
||||
messagesDiv.innerHTML = '<div class="welcome-msg"><div class="welcome-icon">✿</div><h3>欢迎使用 CC-Web</h3><p>开始与 Claude Code 对话</p></div>';
|
||||
return;
|
||||
}
|
||||
// Batch render: last 10 first, then next 20, then the rest
|
||||
const batches = [];
|
||||
const len = messages.length;
|
||||
if (len <= 10) {
|
||||
batches.push([0, len]);
|
||||
} else if (len <= 30) {
|
||||
batches.push([len - 10, len]);
|
||||
batches.push([0, len - 10]);
|
||||
} else {
|
||||
batches.push([len - 10, len]);
|
||||
batches.push([len - 30, len - 10]);
|
||||
batches.push([0, len - 30]);
|
||||
}
|
||||
messagesDiv.appendChild(el);
|
||||
}
|
||||
|
||||
// Render first batch immediately
|
||||
const frag0 = document.createDocumentFragment();
|
||||
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i]));
|
||||
messagesDiv.appendChild(frag0);
|
||||
scrollToBottom();
|
||||
|
||||
// Render remaining batches asynchronously, prepending each
|
||||
let delay = 0;
|
||||
for (let b = 1; b < batches.length; b++) {
|
||||
const [start, end] = batches[b];
|
||||
delay += 16;
|
||||
setTimeout(() => {
|
||||
if (renderEpoch !== epoch) return; // session switched, abort stale render
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i]));
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
updateScrollbar();
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAskUserInput(input) {
|
||||
@@ -530,6 +578,8 @@
|
||||
if (!streamEl) return;
|
||||
const bubble = streamEl.querySelector('.msg-bubble');
|
||||
if (!bubble) return;
|
||||
let toolsDiv = bubble.querySelector('.msg-tools');
|
||||
if (!toolsDiv) { toolsDiv = bubble; }
|
||||
|
||||
const details = document.createElement('details');
|
||||
details.className = 'tool-call';
|
||||
@@ -542,7 +592,7 @@
|
||||
details.appendChild(summary);
|
||||
details.appendChild(buildToolContentElement(name, input));
|
||||
|
||||
bubble.appendChild(details);
|
||||
toolsDiv.appendChild(details);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -560,6 +610,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteConfirm(onConfirm) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'settings-overlay';
|
||||
overlay.style.zIndex = '10002';
|
||||
|
||||
const box = document.createElement('div');
|
||||
box.className = 'settings-panel';
|
||||
box.innerHTML = `
|
||||
<div style="font-size:0.9em;color:var(--text-primary);margin-bottom:20px;line-height:1.7">删除本会话将同步删去本地 Claude 中的会话历史,不可恢复。确认删除?</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<button id="del-confirm-ok" style="width:100%;padding:10px;border:none;border-radius:10px;background:var(--accent);color:#fff;font-size:0.95em;font-weight:600;cursor:pointer;font-family:inherit">确认删除</button>
|
||||
<button id="del-confirm-skip" style="width:100%;padding:9px;border:1px solid var(--border-color);border-radius:10px;background:var(--bg-tertiary);color:var(--text-secondary);font-size:0.85em;cursor:pointer;font-family:inherit">确认且不再提示</button>
|
||||
<button id="del-confirm-cancel" style="width:100%;padding:9px;border:none;border-radius:10px;background:transparent;color:var(--text-muted);font-size:0.85em;cursor:pointer;font-family:inherit">取消</button>
|
||||
</div>
|
||||
`;
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const close = () => document.body.removeChild(overlay);
|
||||
box.querySelector('#del-confirm-ok').addEventListener('click', () => { close(); onConfirm(); });
|
||||
box.querySelector('#del-confirm-skip').addEventListener('click', () => {
|
||||
skipDeleteConfirm = true;
|
||||
localStorage.setItem('cc-web-skip-delete-confirm', '1');
|
||||
close();
|
||||
onConfirm();
|
||||
});
|
||||
box.querySelector('#del-confirm-cancel').addEventListener('click', close);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||
}
|
||||
|
||||
function appendSystemMessage(message) {
|
||||
const welcome = messagesDiv.querySelector('.welcome-msg');
|
||||
if (welcome) welcome.remove();
|
||||
@@ -578,10 +658,73 @@
|
||||
function scrollToBottom() {
|
||||
requestAnimationFrame(() => {
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
updateScrollbar();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Session List ---
|
||||
// --- Custom Scrollbar ---
|
||||
const scrollbarEl = document.getElementById('custom-scrollbar');
|
||||
const thumbEl = document.getElementById('custom-scrollbar-thumb');
|
||||
|
||||
function updateScrollbar() {
|
||||
if (!scrollbarEl || !thumbEl) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesDiv;
|
||||
if (scrollHeight <= clientHeight) {
|
||||
thumbEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
thumbEl.style.display = '';
|
||||
const trackH = scrollbarEl.clientHeight;
|
||||
const thumbH = Math.max(30, trackH * clientHeight / scrollHeight);
|
||||
const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackH - thumbH);
|
||||
thumbEl.style.height = thumbH + 'px';
|
||||
thumbEl.style.top = thumbTop + 'px';
|
||||
}
|
||||
|
||||
messagesDiv.addEventListener('scroll', () => updateScrollbar(), { passive: true });
|
||||
new ResizeObserver(updateScrollbar).observe(messagesDiv);
|
||||
|
||||
// Drag logic
|
||||
let dragStartY = 0, dragStartScrollTop = 0, isDragging = false;
|
||||
|
||||
function onDragStart(e) {
|
||||
isDragging = true;
|
||||
dragStartY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;
|
||||
dragStartScrollTop = messagesDiv.scrollTop;
|
||||
thumbEl.classList.add('dragging');
|
||||
scrollbarEl.classList.add('active');
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onDragMove(e) {
|
||||
if (!isDragging) return;
|
||||
const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;
|
||||
const dy = clientY - dragStartY;
|
||||
const { scrollHeight, clientHeight } = messagesDiv;
|
||||
const trackH = scrollbarEl.clientHeight;
|
||||
const thumbH = Math.max(30, trackH * clientHeight / scrollHeight);
|
||||
const ratio = (scrollHeight - clientHeight) / (trackH - thumbH);
|
||||
messagesDiv.scrollTop = dragStartScrollTop + dy * ratio;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
thumbEl.classList.remove('dragging');
|
||||
scrollbarEl.classList.remove('active');
|
||||
}
|
||||
|
||||
thumbEl.addEventListener('mousedown', onDragStart);
|
||||
thumbEl.addEventListener('touchstart', onDragStart, { passive: false });
|
||||
document.addEventListener('mousemove', onDragMove);
|
||||
document.addEventListener('touchmove', onDragMove, { passive: false });
|
||||
document.addEventListener('mouseup', onDragEnd);
|
||||
document.addEventListener('touchend', onDragEnd);
|
||||
|
||||
updateScrollbar();
|
||||
|
||||
|
||||
function renderSessionList() {
|
||||
sessionList.innerHTML = '';
|
||||
for (const s of sessions) {
|
||||
@@ -602,7 +745,7 @@
|
||||
const target = e.target;
|
||||
if (target.classList.contains('delete')) {
|
||||
e.stopPropagation();
|
||||
if (confirm('删除此会话?')) {
|
||||
const doDelete = () => {
|
||||
send({ type: 'delete_session', sessionId: s.id });
|
||||
if (s.id === currentSessionId) {
|
||||
currentSessionId = null;
|
||||
@@ -610,6 +753,11 @@
|
||||
chatTitle.textContent = '新会话';
|
||||
costDisplay.textContent = '';
|
||||
}
|
||||
};
|
||||
if (skipDeleteConfirm) {
|
||||
doDelete();
|
||||
} else {
|
||||
showDeleteConfirm(doDelete);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<span id="cost-display" class="cost-display"></span>
|
||||
</header>
|
||||
|
||||
<div class="messages-wrap">
|
||||
<div id="messages" class="messages">
|
||||
<div class="welcome-msg">
|
||||
<div class="welcome-icon">✿</div>
|
||||
@@ -63,6 +64,10 @@
|
||||
<p>开始与 Claude Code 对话</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-scrollbar" id="custom-scrollbar">
|
||||
<div class="custom-scrollbar-thumb" id="custom-scrollbar-thumb"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slash command menu -->
|
||||
<div id="cmd-menu" class="cmd-menu" hidden></div>
|
||||
|
||||
@@ -334,17 +334,60 @@ body {
|
||||
}
|
||||
|
||||
/* === Messages === */
|
||||
.messages {
|
||||
.messages-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.messages {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding: 16px;
|
||||
padding-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.messages::-webkit-scrollbar { display: none; }
|
||||
/* Custom scrollbar */
|
||||
.custom-scrollbar {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.messages-wrap:hover .custom-scrollbar,
|
||||
.custom-scrollbar.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.custom-scrollbar-thumb {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
min-height: 30px;
|
||||
border-radius: 4px;
|
||||
background: var(--scrollbar-thumb);
|
||||
cursor: grab;
|
||||
transition: width 0.15s, right 0.15s, background 0.15s;
|
||||
pointer-events: all;
|
||||
}
|
||||
.custom-scrollbar-thumb:hover,
|
||||
.custom-scrollbar-thumb.dragging {
|
||||
width: 12px;
|
||||
right: -3px;
|
||||
background: #b0a090;
|
||||
cursor: grab;
|
||||
}
|
||||
.custom-scrollbar-thumb.dragging { cursor: grabbing; }
|
||||
.welcome-msg {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
@@ -519,33 +562,6 @@ body {
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
}
|
||||
/* HTML preview */
|
||||
.code-html-preview {
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
.code-html-preview summary {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
}
|
||||
.code-html-preview summary::-webkit-details-marker { display: none; }
|
||||
.code-html-preview summary::before {
|
||||
content: '▸';
|
||||
font-size: 11px;
|
||||
transition: transform 0.2s;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.code-html-preview[open] summary::before { transform: rotate(90deg); }
|
||||
.code-html-preview iframe {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Tool calls */
|
||||
.tool-call {
|
||||
|
||||
65
server.js
65
server.js
@@ -309,6 +309,27 @@ function loadClaudeJsonModelMap() {
|
||||
}
|
||||
|
||||
// Apply model config to runtime MODEL_MAP only (env vars are injected per-spawn, not here)
|
||||
const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'settings.json');
|
||||
const SETTINGS_API_KEYS = ['ANTHROPIC_AUTH_TOKEN','ANTHROPIC_API_KEY','ANTHROPIC_BASE_URL','ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL','ANTHROPIC_DEFAULT_SONNET_MODEL','ANTHROPIC_DEFAULT_HAIKU_MODEL'];
|
||||
|
||||
function applyCustomTemplateToSettings(tpl) {
|
||||
let settings = {};
|
||||
try { settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8')); } catch {}
|
||||
const cleanedEnv = {};
|
||||
for (const [k, v] of Object.entries(settings.env || {})) {
|
||||
if (!SETTINGS_API_KEYS.includes(k)) cleanedEnv[k] = v;
|
||||
}
|
||||
if (tpl.apiKey) { cleanedEnv.ANTHROPIC_AUTH_TOKEN = tpl.apiKey; cleanedEnv.ANTHROPIC_API_KEY = tpl.apiKey; }
|
||||
if (tpl.apiBase) cleanedEnv.ANTHROPIC_BASE_URL = tpl.apiBase;
|
||||
if (tpl.defaultModel) cleanedEnv.ANTHROPIC_MODEL = tpl.defaultModel;
|
||||
if (tpl.opusModel) cleanedEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = tpl.opusModel;
|
||||
if (tpl.sonnetModel) cleanedEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = tpl.sonnetModel;
|
||||
if (tpl.haikuModel) cleanedEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = tpl.haikuModel;
|
||||
settings.env = cleanedEnv;
|
||||
try { fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2)); } catch {}
|
||||
}
|
||||
|
||||
function applyModelConfig() {
|
||||
const config = loadModelConfig();
|
||||
if (config.mode === 'custom' && config.activeTemplate) {
|
||||
@@ -934,6 +955,11 @@ function handleSaveModelConfig(ws, newConfig) {
|
||||
// Re-apply at runtime
|
||||
MODEL_MAP = { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001' };
|
||||
applyModelConfig();
|
||||
// custom mode: write to ~/.claude/settings.json immediately on save
|
||||
if (merged.mode === 'custom' && merged.activeTemplate) {
|
||||
const tpl = merged.templates.find(t => t.name === merged.activeTemplate);
|
||||
if (tpl) applyCustomTemplateToSettings(tpl);
|
||||
}
|
||||
plog('INFO', 'model_config_saved', { mode: merged.mode, activeTemplate: merged.activeTemplate });
|
||||
wsSend(ws, { type: 'model_config', config: getModelConfigMasked() });
|
||||
wsSend(ws, { type: 'system_message', message: '模型配置已保存' });
|
||||
@@ -1138,7 +1164,23 @@ function handleDeleteSession(ws, sessionId) {
|
||||
cleanRunDir(sessionId);
|
||||
try {
|
||||
const p = sessionPath(sessionId);
|
||||
// Read claudeSessionId before deleting the file
|
||||
let claudeSessionId = null;
|
||||
try {
|
||||
const session = loadSession(sessionId);
|
||||
claudeSessionId = session?.claudeSessionId || null;
|
||||
} catch {}
|
||||
if (fs.existsSync(p)) fs.unlinkSync(p);
|
||||
// Sync-delete the corresponding Claude native session .jsonl
|
||||
if (claudeSessionId) {
|
||||
const projectsDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'projects');
|
||||
try {
|
||||
for (const proj of fs.readdirSync(projectsDir)) {
|
||||
const target = path.join(projectsDir, proj, `${claudeSessionId}.jsonl`);
|
||||
if (fs.existsSync(target)) fs.unlinkSync(target);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
sendSessionList(ws);
|
||||
} catch {
|
||||
wsSend(ws, { type: 'error', message: 'Failed to delete session' });
|
||||
@@ -1295,28 +1337,7 @@ function handleMessage(ws, msg, options = {}) {
|
||||
const modelCfg = loadModelConfig();
|
||||
if (modelCfg.mode === 'custom' && modelCfg.activeTemplate) {
|
||||
const tpl = (modelCfg.templates || []).find(t => t.name === modelCfg.activeTemplate);
|
||||
if (tpl) {
|
||||
const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'settings.json');
|
||||
let settings = {};
|
||||
try { settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8')); } catch {}
|
||||
const API_KEYS = ['ANTHROPIC_AUTH_TOKEN','ANTHROPIC_API_KEY','ANTHROPIC_BASE_URL','ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL','ANTHROPIC_DEFAULT_SONNET_MODEL','ANTHROPIC_DEFAULT_HAIKU_MODEL'];
|
||||
const existingEnv = settings.env || {};
|
||||
// Remove old API-related keys, keep non-API keys
|
||||
const cleanedEnv = {};
|
||||
for (const [k, v] of Object.entries(existingEnv)) {
|
||||
if (!API_KEYS.includes(k)) cleanedEnv[k] = v;
|
||||
}
|
||||
// Inject template values
|
||||
if (tpl.apiKey) { cleanedEnv.ANTHROPIC_AUTH_TOKEN = tpl.apiKey; cleanedEnv.ANTHROPIC_API_KEY = tpl.apiKey; }
|
||||
if (tpl.apiBase) cleanedEnv.ANTHROPIC_BASE_URL = tpl.apiBase;
|
||||
if (tpl.defaultModel) cleanedEnv.ANTHROPIC_MODEL = tpl.defaultModel;
|
||||
if (tpl.opusModel) cleanedEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = tpl.opusModel;
|
||||
if (tpl.sonnetModel) cleanedEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = tpl.sonnetModel;
|
||||
if (tpl.haikuModel) cleanedEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = tpl.haikuModel;
|
||||
settings.env = cleanedEnv;
|
||||
try { fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2)); } catch {}
|
||||
}
|
||||
if (tpl) applyCustomTemplateToSettings(tpl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user