-
✿
-
欢迎使用 CC-Web
-
开始与 Claude Code 对话
+
+
+
+
✿
+
欢迎使用 CC-Web
+
开始与 Claude Code 对话
+
+
+
diff --git a/public/style.css b/public/style.css
index 2361745..4fab7e7 100644
--- a/public/style.css
+++ b/public/style.css
@@ -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 {
diff --git a/server.js b/server.js
index 2dd72b9..c4477fe 100644
--- a/server.js
+++ b/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);
}
}