diff --git a/web/static/css/style.css b/web/static/css/style.css
index 94428ab8..3de68c95 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -14880,6 +14880,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
position: relative;
flex-shrink: 0;
}
+.ws-project-selector-wrapper {
+ position: relative;
+ flex-shrink: 0;
+}
.ws-agent-mode-wrapper {
flex-shrink: 0;
}
diff --git a/web/static/js/projects.js b/web/static/js/projects.js
index 49d6dfb6..64d24a28 100644
--- a/web/static/js/projects.js
+++ b/web/static/js/projects.js
@@ -1270,12 +1270,18 @@ async function saveProjectModal() {
return;
}
const fromChat = !!window._projectModalFromChat;
+ const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
window._projectModalFromChat = false;
+ window._projectModalFromWebshellConnId = '';
closeProjectModal();
const saved = await res.json();
await loadProjectsList();
if (saved.id) {
- if (fromChat && !editId) {
+ if (fromWebshellConnId && !editId) {
+ if (typeof applyWebshellAiProjectSelection === 'function') {
+ await applyWebshellAiProjectSelection(saved.id);
+ }
+ } else if (fromChat && !editId) {
await applyChatProjectSelection(saved.id);
} else {
await selectProject(saved.id);
diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js
index c06f82a0..fee81d99 100644
--- a/web/static/js/webshell.js
+++ b/web/static/js/webshell.js
@@ -27,6 +27,9 @@ const WEBSHELL_HISTORY_MAX = 100;
let webshellClearInProgress = false;
// AI 助手:按连接 ID 保存对话 ID,便于多轮对话
let webshellAiConvMap = {};
+// AI 助手:项目绑定(已有对话按 convId,新对话按 connId 草稿)
+let webshellAiProjectByConvId = {};
+let webshellAiDraftProjectByConn = {};
let webshellAiSending = false;
let webshellAiAbortController = null; // AbortController for current AI stream
let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader
@@ -266,6 +269,7 @@ function wsToggleRolePanel() {
var isOpen = panel.style.display === 'flex';
if (isOpen) { wsCloseRolePanel(); return; }
wsCloseAgentModePanel();
+ wsCloseProjectPanel();
panel.style.display = 'flex';
}
function wsCloseRolePanel() {
@@ -340,6 +344,7 @@ function wsToggleAgentModePanel() {
var isOpen = panel.style.display === 'flex';
if (isOpen) { wsCloseAgentModePanel(); return; }
wsCloseRolePanel();
+ wsCloseProjectPanel();
panel.style.display = 'flex';
}
function wsCloseAgentModePanel() {
@@ -347,10 +352,204 @@ function wsCloseAgentModePanel() {
if (panel) panel.style.display = 'none';
}
+// ─── WebShell AI 项目选择器(与主「对话」页对齐) ───
+
+function wsProjectT(key, fallback) {
+ if (typeof window.t === 'function') {
+ var v = window.t(key);
+ if (v && v !== key) return v;
+ }
+ return fallback;
+}
+
+function getWebshellAiConvId(conn) {
+ if (!conn || !conn.id) return '';
+ return webshellAiConvMap[conn.id] || '';
+}
+
+function getWebshellAiProjectSelection(conn) {
+ if (!conn || !conn.id) return '';
+ var convId = getWebshellAiConvId(conn);
+ if (convId) return webshellAiProjectByConvId[convId] || '';
+ return webshellAiDraftProjectByConn[conn.id] || '';
+}
+
+function wsSetWebshellAiProject(conn, projectId) {
+ if (!conn || !conn.id) return;
+ var pid = projectId || '';
+ var convId = getWebshellAiConvId(conn);
+ if (convId) {
+ if (pid) webshellAiProjectByConvId[convId] = pid;
+ else delete webshellAiProjectByConvId[convId];
+ } else if (pid) {
+ webshellAiDraftProjectByConn[conn.id] = pid;
+ } else {
+ delete webshellAiDraftProjectByConn[conn.id];
+ }
+ wsUpdateProjectButtonLabel();
+}
+
+function wsIsActiveProjectId(id) {
+ if (!id) return false;
+ var map = window.projectNameById || {};
+ return !!map[id];
+}
+
+function wsResolveWebshellAiProjectSelection(conn) {
+ var raw = getWebshellAiProjectSelection(conn);
+ if (!raw) return '';
+ return wsIsActiveProjectId(raw) ? raw : '';
+}
+
+function wsUpdateProjectButtonLabel() {
+ var textEl = document.getElementById('ws-project-text');
+ if (!textEl || !webshellCurrentConn) return;
+ var id = wsResolveWebshellAiProjectSelection(webshellCurrentConn);
+ var nameMap = window.projectNameById || {};
+ textEl.textContent = id && nameMap[id] ? nameMap[id] : wsProjectT('projects.noProject', '无项目');
+}
+
+async function wsRenderProjectPanelList() {
+ var list = document.getElementById('ws-project-list');
+ if (!list || !webshellCurrentConn) return;
+ var conn = webshellCurrentConn;
+ var selected = wsResolveWebshellAiProjectSelection(conn);
+ var projects = [];
+ try {
+ if (typeof window.fetchAllProjects === 'function') {
+ projects = await window.fetchAllProjects(false);
+ }
+ } catch (e) {
+ list.innerHTML = '
' + escapeHtml(wsProjectT('projects.loadFailedRetry', '加载失败,请重试')) + '
';
+ return;
+ }
+ if (typeof window.rebuildProjectNameMap === 'function') {
+ window.rebuildProjectNameMap(projects);
+ }
+ var activeProjects = projects.filter(function (p) { return p.status !== 'archived'; });
+ var items = [{ id: '', name: wsProjectT('projects.noProject', '无项目'), description: wsProjectT('projects.noProjectDescription', '不绑定项目') }].concat(activeProjects);
+ list.innerHTML = '';
+ items.forEach(function (p) {
+ var isNone = !p.id;
+ var isSelected = isNone ? !selected : selected === p.id;
+ var desc = isNone
+ ? (p.description || '')
+ : ((p.description || '').trim().slice(0, 80) || wsProjectT('projects.sharedFactBoard', '共享事实黑板'));
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
+ btn.setAttribute('role', 'option');
+ btn.onclick = function () { wsSelectProject(p.id || ''); };
+ btn.innerHTML = '' + (isNone ? '—' : '📁') + '
' +
+ '' +
+ '
' + escapeHtml(p.name || '未命名') + '
' +
+ '
' + escapeHtml(desc) + '
' +
+ (isSelected ? '✓
' : '');
+ list.appendChild(btn);
+ });
+}
+
+async function wsRenderProjectPanel() {
+ var list = document.getElementById('ws-project-list');
+ if (!list) return;
+ list.innerHTML = '' + escapeHtml(wsProjectT('common.loading', '加载中...')) + '
';
+ await wsRenderProjectPanelList();
+}
+
+function wsCloseProjectPanel() {
+ var panel = document.getElementById('ws-project-panel');
+ var btn = document.getElementById('ws-project-btn');
+ if (panel) panel.style.display = 'none';
+ if (btn) {
+ btn.classList.remove('active');
+ btn.setAttribute('aria-expanded', 'false');
+ }
+}
+
+async function wsToggleProjectPanel() {
+ var panel = document.getElementById('ws-project-panel');
+ var btn = document.getElementById('ws-project-btn');
+ if (!panel) return;
+ var isHidden = panel.style.display === 'none' || !panel.style.display;
+ if (!isHidden) {
+ wsCloseProjectPanel();
+ return;
+ }
+ wsCloseRolePanel();
+ wsCloseAgentModePanel();
+ panel.style.display = 'flex';
+ if (btn) {
+ btn.classList.add('active');
+ btn.setAttribute('aria-expanded', 'true');
+ }
+ await wsRenderProjectPanel();
+}
+
+async function wsSelectProject(projectId) {
+ wsCloseProjectPanel();
+ await applyWebshellAiProjectSelection(projectId || '');
+}
+
+async function applyWebshellAiProjectSelection(projectId) {
+ var conn = webshellCurrentConn;
+ if (!conn || !conn.id) return;
+ var prev = getWebshellAiProjectSelection(conn);
+ if (projectId === prev) {
+ wsUpdateProjectButtonLabel();
+ return;
+ }
+ var convId = getWebshellAiConvId(conn);
+ if (convId) {
+ try {
+ var res = await apiFetch('/api/conversations/' + encodeURIComponent(convId) + '/project', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ projectId: projectId }),
+ });
+ if (!res.ok) {
+ var err = await res.json().catch(function () { return {}; });
+ throw new Error(err.error || res.statusText);
+ }
+ wsSetWebshellAiProject(conn, projectId);
+ if (typeof showNotification === 'function') {
+ showNotification(
+ projectId ? wsProjectT('projects.projectBound', '已绑定项目') : wsProjectT('projects.projectUnbound', '已解除项目绑定'),
+ 'success'
+ );
+ }
+ } catch (e) {
+ console.error(e);
+ alert(wsProjectT('projects.updateProjectBindingFailed', '更新项目绑定失败') + ': ' + (e.message || e));
+ wsUpdateProjectButtonLabel();
+ return;
+ }
+ } else {
+ wsSetWebshellAiProject(conn, projectId);
+ }
+ wsUpdateProjectButtonLabel();
+}
+
+function showNewProjectModalFromWebshellAi() {
+ wsCloseProjectPanel();
+ if (webshellCurrentConn && webshellCurrentConn.id) {
+ window._projectModalFromWebshellConnId = webshellCurrentConn.id;
+ }
+ window._projectModalFromChat = false;
+ if (typeof showNewProjectModal === 'function') showNewProjectModal();
+}
+
+window.applyWebshellAiProjectSelection = applyWebshellAiProjectSelection;
+window.showNewProjectModalFromWebshellAi = showNewProjectModalFromWebshellAi;
+window.wsToggleProjectPanel = wsToggleProjectPanel;
+window.wsCloseProjectPanel = wsCloseProjectPanel;
+
+// ─── end WebShell AI 项目选择器 ───
+
/** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */
function wsRefreshSelectors() {
wsUpdateRoleSelectorDisplay();
wsRenderRoleList();
+ wsUpdateProjectButtonLabel();
var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'eino_single';
if (stored !== 'eino_single' && stored !== 'deep' && stored !== 'plan_execute' && stored !== 'supervisor') {
stored = 'eino_single';
@@ -370,6 +569,11 @@ document.addEventListener('click', function (e) {
if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) {
wsCloseAgentModePanel();
}
+ var projectPanel = document.getElementById('ws-project-panel');
+ var projectBtn = document.getElementById('ws-project-btn');
+ if (projectPanel && projectPanel.style.display !== 'none' && projectBtn && !projectPanel.contains(e.target) && !projectBtn.contains(e.target)) {
+ wsCloseProjectPanel();
+ }
});
// ─── end WebShell AI 选择器 ───
@@ -1873,6 +2077,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' })
.then(function (r) { return r.json(); })
.then(function (data) {
+ wsSetWebshellAiProject(conn, data.projectId || data.project_id || '');
messagesContainer.innerHTML = '';
var list = data.messages || [];
list.forEach(function (msg) {
@@ -2003,6 +2208,25 @@ function selectWebshell(id, stateReady) {
'' +
'