diff --git a/lib/agent-runtime.js b/lib/agent-runtime.js
index ee9551d..ca759b6 100644
--- a/lib/agent-runtime.js
+++ b/lib/agent-runtime.js
@@ -70,29 +70,29 @@ function createAgentRuntime(deps) {
const runtimeConfig = prepareCodexCustomRuntime(codexConfig);
if (runtimeConfig?.error) {
return { error: runtimeConfig.error };
- }
- const runtimeId = getRuntimeSessionId(session);
- const args = ['exec'];
- args.push('--json', '--skip-git-repo-check');
+ }
+ const runtimeId = getRuntimeSessionId(session);
+ const args = ['exec'];
+ args.push('--json', '--skip-git-repo-check');
- const permMode = session.permissionMode || 'yolo';
- // `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`.
- // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors
- // with: "unexpected argument '-s' found".
- if (runtimeId && permMode === 'plan') {
- args.push('-s', 'read-only');
- }
- if (runtimeId) args.push('resume');
- switch (permMode) {
- case 'yolo':
- args.push('--dangerously-bypass-approvals-and-sandbox');
- break;
- case 'plan':
- if (!runtimeId) args.push('-s', 'read-only');
- break;
- case 'default':
- default:
- args.push('--full-auto');
+ const permMode = session.permissionMode || 'yolo';
+ // `-s/--sandbox` is an option for `codex exec`, but not for `codex exec resume`.
+ // When resuming, it must appear before the `resume` subcommand, otherwise Codex CLI errors
+ // with: "unexpected argument '-s' found".
+ if (runtimeId && permMode === 'plan') {
+ args.push('-s', 'read-only');
+ }
+ if (runtimeId) args.push('resume');
+ switch (permMode) {
+ case 'yolo':
+ args.push('--dangerously-bypass-approvals-and-sandbox');
+ break;
+ case 'plan':
+ if (!runtimeId) args.push('-s', 'read-only');
+ break;
+ case 'default':
+ default:
+ args.push('--full-auto');
break;
}
diff --git a/public/app.js b/public/app.js
index 3d29f91..1a62566 100644
--- a/public/app.js
+++ b/public/app.js
@@ -11,6 +11,7 @@
{ cmd: '/mode', desc: '查看/切换权限模式' },
{ cmd: '/cost', desc: '查看会话费用' },
{ cmd: '/compact', desc: '压缩上下文' },
+ { cmd: '/init', desc: '生成/更新 CLAUDE.md' },
{ cmd: '/help', desc: '显示帮助' },
];
@@ -3260,13 +3261,13 @@
- 系统
-
-
-
-
-
- `;
+ 系统
+
+
+
+
+
+ `;
overlay.appendChild(panel);
document.body.appendChild(overlay);
@@ -3281,9 +3282,9 @@
const codexStatus = panel.querySelector('#codex-status');
const codexSaveBtn = panel.querySelector('#codex-save-btn');
- const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn');
- const checkUpdateBtn = panel.querySelector('#check-update-btn');
- const updateStatusEl = panel.querySelector('#update-status');
+ const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn');
+ const checkUpdateBtn = panel.querySelector('#check-update-btn');
+ const updateStatusEl = panel.querySelector('#update-status');
let currentCodexConfig = null;
let codexEditingProfiles = [];
@@ -3822,12 +3823,12 @@
renderModelCustomArea();
};
- // === Notify Config UI (moved to subpage) ===
- // notify config is handled by openNotifySubpage()
+ // === Notify Config UI (moved to subpage) ===
+ // notify config is handled by openNotifySubpage()
- const closeBtn = panel.querySelector('.settings-close');
- const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn');
- pwOpenModalBtn.addEventListener('click', openPasswordModal);
+ const closeBtn = panel.querySelector('.settings-close');
+ const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn');
+ pwOpenModalBtn.addEventListener('click', openPasswordModal);
// Check update button
const checkUpdateBtn = panel.querySelector('#check-update-btn');
diff --git a/public/claude.png b/public/claude.png
new file mode 100644
index 0000000..ae85771
Binary files /dev/null and b/public/claude.png differ
diff --git a/public/codex.png b/public/codex.png
new file mode 100644
index 0000000..7f5f24d
Binary files /dev/null and b/public/codex.png differ
diff --git a/public/style.css b/public/style.css
index 5aed2b0..ef4a478 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1021,10 +1021,10 @@ body.session-loading-active {
background: transparent;
border: none;
}
-/* Codex avatar: GPT logo on green bg */
+/* Codex avatar: transparent bg to match the supplied asset */
.msg.assistant.agent-codex .msg-avatar {
- background: #10a37f;
- color: #fff;
+ background: transparent;
+ border: none;
}
.msg-bubble {
diff --git a/server.js b/server.js
index 6a0bb19..dde6dac 100644
--- a/server.js
+++ b/server.js
@@ -437,27 +437,27 @@ const activeTokens = new Set();
const AUTH_FAIL_WINDOW = 5 * 60 * 1000; // 5 minutes
const AUTH_FAIL_MAX = 3;
const authFailures = new Map(); // ip -> [timestamp, ...]
- let bannedIPs = new Set();
+let bannedIPs = new Set();
- // Tailscale / loopback whitelist — never ban these IPs.
- // Extra whitelist can be provided via env var (comma/space separated):
- // CC_WEB_IP_WHITELIST=","
- const EXTRA_WHITELIST_IPS = new Set(
- String(process.env.CC_WEB_IP_WHITELIST || '')
- .split(/[\s,]+/)
- .map(s => s.trim())
- .filter(Boolean)
- .map(s => s.replace(/^::ffff:/, ''))
- );
+// Tailscale / loopback whitelist — never ban these IPs.
+// Extra whitelist can be provided via env var (comma/space separated):
+// CC_WEB_IP_WHITELIST=","
+const EXTRA_WHITELIST_IPS = new Set(
+ String(process.env.CC_WEB_IP_WHITELIST || '')
+ .split(/[\s,]+/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .map(s => s.replace(/^::ffff:/, ''))
+);
- function isWhitelistedIP(ip) {
- if (!ip) return false;
- const cleaned = ip.replace(/^::ffff:/, '');
- return cleaned === '127.0.0.1'
- || cleaned === '::1'
- || cleaned.startsWith('100.')
- || EXTRA_WHITELIST_IPS.has(cleaned);
- }
+function isWhitelistedIP(ip) {
+ if (!ip) return false;
+ const cleaned = ip.replace(/^::ffff:/, '');
+ return cleaned === '127.0.0.1'
+ || cleaned === '::1'
+ || cleaned.startsWith('100.')
+ || EXTRA_WHITELIST_IPS.has(cleaned);
+}
function loadBannedIPs() {
try {
@@ -2055,24 +2055,43 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
break;
}
- case '/mode': {
- const modeInput = parts[1];
- const VALID_MODES = ['default', 'plan', 'yolo'];
- const MODE_DESC = { default: '默认(需权限审批,受限操作)', plan: 'Plan(需确认计划后执行)', yolo: 'YOLO(跳过所有权限检查)' };
- if (!modeInput) {
- const cur = session?.permissionMode || 'yolo';
- wsSend(ws, { type: 'system_message', message: `当前模式: ${MODE_DESC[cur] || cur}\n可选: default, plan, yolo` });
- } else if (VALID_MODES.includes(modeInput.toLowerCase())) {
- const mode = modeInput.toLowerCase();
- if (session) {
- session.permissionMode = mode;
- // Mode switching should not reset runtime context (Claude/Codex both resume).
- session.updated = new Date().toISOString();
- saveSession(session);
- }
- wsSend(ws, { type: 'system_message', message: `权限模式已切换为: ${MODE_DESC[mode]}` });
- wsSend(ws, { type: 'mode_changed', mode });
- } else {
+ case '/init': {
+ if (agent !== 'claude') {
+ wsSend(ws, { type: 'system_message', message: '/init 仅支持 Claude,Codex 暂不支持该命令。' });
+ break;
+ }
+ if (!sessionId || !session) {
+ wsSend(ws, { type: 'system_message', message: '请先进入一个会话后再执行 /init。' });
+ break;
+ }
+ if (activeProcesses.has(sessionId)) {
+ wsSend(ws, { type: 'system_message', message: '当前会话正在处理中,请先等待完成或点击停止。' });
+ break;
+ }
+ wsSend(ws, { type: 'system_message', message: '正在分析项目并生成 CLAUDE.md ...' });
+ pendingSlashCommands.set(session.id, { kind: 'init' });
+ handleMessage(ws, { text: '/init', sessionId: session.id, mode: session.permissionMode || 'yolo' }, { hideInHistory: true });
+ break;
+ }
+
+ case '/mode': {
+ const modeInput = parts[1];
+ const VALID_MODES = ['default', 'plan', 'yolo'];
+ const MODE_DESC = { default: '默认(需权限审批,受限操作)', plan: 'Plan(需确认计划后执行)', yolo: 'YOLO(跳过所有权限检查)' };
+ if (!modeInput) {
+ const cur = session?.permissionMode || 'yolo';
+ wsSend(ws, { type: 'system_message', message: `当前模式: ${MODE_DESC[cur] || cur}\n可选: default, plan, yolo` });
+ } else if (VALID_MODES.includes(modeInput.toLowerCase())) {
+ const mode = modeInput.toLowerCase();
+ if (session) {
+ session.permissionMode = mode;
+ // Mode switching should not reset runtime context (Claude/Codex both resume).
+ session.updated = new Date().toISOString();
+ saveSession(session);
+ }
+ wsSend(ws, { type: 'system_message', message: `权限模式已切换为: ${MODE_DESC[mode]}` });
+ wsSend(ws, { type: 'mode_changed', mode });
+ } else {
wsSend(ws, { type: 'system_message', message: `无效模式: ${modeInput}\n可选: default, plan, yolo` });
}
break;
@@ -2088,7 +2107,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
type: 'system_message',
message: agent === 'codex'
? base + '\n/model [名称] — 查看/切换 Codex 模型(自由输入)\n/compact — 执行 Codex /compact 压缩上下文'
- : base + '\n/model [名称] — 查看/切换模型(opus, sonnet, haiku)\n/compact — 执行 Claude 原生上下文压缩(保留压缩计划并可自动续跑)',
+ : base + '\n/model [名称] — 查看/切换模型(opus, sonnet, haiku)\n/compact — 执行 Claude 原生上下文压缩(保留压缩计划并可自动续跑)\n/init — 分析项目并生成/更新 CLAUDE.md',
});
break;
}
@@ -2330,20 +2349,20 @@ function handleRenameSession(ws, sessionId, title) {
}
}
- function handleSetMode(ws, sessionId, mode) {
- const VALID_MODES = ['default', 'plan', 'yolo'];
- if (!mode || !VALID_MODES.includes(mode)) return;
- if (sessionId) {
- const session = loadSession(sessionId);
- if (session) {
- session.permissionMode = mode;
- // Same rule as /mode: don't clear runtime context on mode changes.
- session.updated = new Date().toISOString();
- saveSession(session);
- }
- }
- wsSend(ws, { type: 'mode_changed', mode });
- }
+ function handleSetMode(ws, sessionId, mode) {
+ const VALID_MODES = ['default', 'plan', 'yolo'];
+ if (!mode || !VALID_MODES.includes(mode)) return;
+ if (sessionId) {
+ const session = loadSession(sessionId);
+ if (session) {
+ session.permissionMode = mode;
+ // Same rule as /mode: don't clear runtime context on mode changes.
+ session.updated = new Date().toISOString();
+ saveSession(session);
+ }
+ }
+ wsSend(ws, { type: 'mode_changed', mode });
+ }
function handleDisconnect(ws, wsId) {
const affectedSessions = [];