- Split server/chat.py, server/status.py, server/tasks.py into sub-packages
- Split utils/context_manager.py, utils/conversation_manager.py into mixin packages
- Split modules/file_manager.py, persistent_terminal.py, terminal_ops.py, mcp_client_manager.py into packages
- Split core/main_terminal_parts/context.py into base + mixins
- Split static/src/app/methods/{ui,message,upload,taskPolling,conversation} into sub-modules
- Fix flattened method exports, dynamic import paths, missing cross-module imports
- Add missing permission/network/execution mode constants and _PERMISSION_MODE_LABEL
- Update AGENTS.md, CLAUDE.md and project memories to reflect new structure
399 lines
16 KiB
TypeScript
399 lines
16 KiB
TypeScript
// @ts-nocheck
|
|
import { debugLog } from '../common';
|
|
import { usePolicyStore } from '../../../stores/policy';
|
|
import { useModelStore } from '../../../stores/model';
|
|
import { usePersonalizationStore } from '../../../stores/personalization';
|
|
import { useTutorialStore } from '../../../stores/tutorial';
|
|
import { renderMarkdown as renderMarkdownHelper } from '../../../composables/useMarkdownRenderer';
|
|
import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from '../../../composables/useScrollControl';
|
|
import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from '../../../composables/usePanelResize';
|
|
import {
|
|
SUB_AGENT_DONE_PREFIX_RE,
|
|
BG_RUN_COMMAND_DONE_PREFIX_RE,
|
|
userMDebug,
|
|
UI_BOUNCE_TRACE_MAX,
|
|
uiBounceTraceLastTsByKey,
|
|
isUiBounceTraceEnabled,
|
|
uiBounceTrace,
|
|
isConnectionDiagEnabled,
|
|
pushConnectionDiagRecord,
|
|
connectionDiag,
|
|
parseSubAgentDoneLabel,
|
|
parseBackgroundRunCommandDoneLabel,
|
|
parseSystemNoticeLabel,
|
|
} from './shared';
|
|
|
|
export const hostWorkspaceMethods = {
|
|
async fetchHostWorkspaces() {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
this.hostWorkspaces = [];
|
|
this.currentHostWorkspaceId = '';
|
|
this.defaultHostWorkspaceId = '';
|
|
return;
|
|
}
|
|
try {
|
|
const resp = await fetch(this.versioningHostMode ? '/api/host/workspaces' : '/api/projects');
|
|
const payload = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !payload?.success) {
|
|
throw new Error(payload?.error || '获取项目列表失败');
|
|
}
|
|
const data = payload.data || {};
|
|
const items = Array.isArray(data.workspaces) ? data.workspaces : [];
|
|
this.hostWorkspaces = items.map((item: any) => ({
|
|
workspace_id: String(item.workspace_id || ''),
|
|
label: String(item.label || item.workspace_id || '未命名项目'),
|
|
path: String(item.path || ''),
|
|
is_current: !!item.is_current,
|
|
running_task_count: Number(item.running_task_count || 0)
|
|
}));
|
|
this.currentHostWorkspaceId = String(data.current_workspace_id || '');
|
|
this.defaultHostWorkspaceId = String(data.default_workspace_id || '');
|
|
await this.refreshRunningWorkspaceTasks();
|
|
} catch (error) {
|
|
console.warn('加载工作区/项目失败:', error);
|
|
this.hostWorkspaces = [];
|
|
this.currentHostWorkspaceId = '';
|
|
this.defaultHostWorkspaceId = '';
|
|
this.runningWorkspaceTasks = [];
|
|
}
|
|
},
|
|
async handleHostWorkspaceSwitch(workspaceId) {
|
|
const targetId = String(workspaceId || '').trim();
|
|
if (!targetId || this.hostWorkspaceSwitching) {
|
|
return;
|
|
}
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
if (targetId === this.currentHostWorkspaceId) {
|
|
return;
|
|
}
|
|
|
|
this.hostWorkspaceSwitching = true;
|
|
const previousConversationList = Array.isArray(this.conversations) ? [...this.conversations] : [];
|
|
const previousSearchActive = this.searchActive;
|
|
this.searchActive = false;
|
|
this.searchResults = [];
|
|
this.conversations = [];
|
|
this.conversationsOffset = 0;
|
|
this.hasMoreConversations = false;
|
|
this.conversationsLoading = true;
|
|
try {
|
|
// 先在“当前工作区作用域”落盘草稿,再切换工作区。
|
|
// 否则会把旧工作区草稿误写到目标工作区作用域里。
|
|
await this.persistComposerDraftNow({
|
|
reason: 'switch-host-workspace:before-select',
|
|
force: true,
|
|
keepalive: true
|
|
}).catch(() => {});
|
|
|
|
const query = encodeURIComponent(targetId);
|
|
const selectEndpoint = this.versioningHostMode ? '/api/host/workspaces/select' : '/api/projects/select';
|
|
const resp = await fetch(`${selectEndpoint}?workspace_id=${query}`);
|
|
const payload = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !payload?.success) {
|
|
throw new Error(payload?.error || (this.versioningHostMode ? '切换宿主机工作区失败' : '切换项目失败'));
|
|
}
|
|
const data = payload.data || {};
|
|
this.currentHostWorkspaceId = String(data.current_workspace_id || targetId);
|
|
if (data.project_path) {
|
|
this.projectPath = String(data.project_path);
|
|
}
|
|
if (data.default_workspace_id) {
|
|
this.defaultHostWorkspaceId = String(data.default_workspace_id);
|
|
}
|
|
this.messages = [];
|
|
this.currentConversationId = null;
|
|
this.resetTokenStatistics?.();
|
|
this.currentConversationTitle = '新对话';
|
|
this.titleReady = true;
|
|
this.suppressTitleTyping = false;
|
|
try {
|
|
const { useTaskStore } = await import('../../../stores/task');
|
|
const taskStore = useTaskStore();
|
|
taskStore.clearTask();
|
|
} catch (_) {}
|
|
this.clearLocalTaskUiState?.('switch-host-workspace');
|
|
await this.fetchHostWorkspaces();
|
|
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
|
|
const activeTask = (Array.isArray(this.runningWorkspaceTasks) ? this.runningWorkspaceTasks : [])
|
|
.filter((task: any) => String(task?.workspace_id || '') === this.currentHostWorkspaceId)
|
|
.find((task: any) => activeStatuses.has(String(task?.status || '')) && task?.conversation_id);
|
|
if (activeTask?.conversation_id) {
|
|
await this.loadConversation(String(activeTask.conversation_id), { force: true });
|
|
this.conversationsOffset = 0;
|
|
await this.loadConversationsList();
|
|
} else {
|
|
this.startTitleTyping('新对话', { animate: false });
|
|
history.replaceState({}, '', '/new');
|
|
await this.loadConversationsList();
|
|
}
|
|
const switchedWorkspace = (Array.isArray(this.hostWorkspaces) ? this.hostWorkspaces : [])
|
|
.find((item: any) => String(item?.workspace_id || '') === this.currentHostWorkspaceId);
|
|
const switchedLabel = String(switchedWorkspace?.label || '').trim();
|
|
this.uiPushToast({
|
|
title: this.versioningHostMode ? '工作区已切换' : '项目已切换',
|
|
message: this.versioningHostMode
|
|
? (data.project_path || switchedLabel || targetId)
|
|
: (switchedLabel || targetId),
|
|
type: 'success'
|
|
});
|
|
this.refreshProjectGitSummary?.();
|
|
this.fetchTerminalCount();
|
|
} catch (error) {
|
|
this.conversations = previousConversationList;
|
|
this.searchActive = previousSearchActive;
|
|
this.conversationsLoading = false;
|
|
const message = error instanceof Error ? error.message : String(error || '切换失败');
|
|
this.uiPushToast({
|
|
title: this.versioningHostMode ? '切换工作区失败' : '切换项目失败',
|
|
message,
|
|
type: 'error'
|
|
});
|
|
} finally {
|
|
this.hostWorkspaceSwitching = false;
|
|
}
|
|
},
|
|
async handleCreateHostWorkspace() {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
|
return;
|
|
}
|
|
this.hostWorkspaceCreatePath = '';
|
|
this.hostWorkspaceCreateLabel = '';
|
|
this.hostWorkspaceCreateError = '';
|
|
this.hostWorkspaceCreateDialogOpen = true;
|
|
},
|
|
async openHostWorkspaceManageDialog() {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
this.hostWorkspaceCreateError = '';
|
|
await this.fetchHostWorkspaces();
|
|
this.hostWorkspaceManageDialogOpen = true;
|
|
},
|
|
async submitHostWorkspaceCreate() {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
|
return;
|
|
}
|
|
const workspacePath = String(this.hostWorkspaceCreatePath || '').trim();
|
|
const label = String(this.hostWorkspaceCreateLabel || '').trim();
|
|
if (this.versioningHostMode && !workspacePath) {
|
|
this.hostWorkspaceCreateError = '路径不能为空';
|
|
return;
|
|
}
|
|
if (this.dockerProjectMode && !label) {
|
|
this.hostWorkspaceCreateError = '项目名称不能为空';
|
|
return;
|
|
}
|
|
|
|
this.hostWorkspaceCreateSubmitting = true;
|
|
this.hostWorkspaceCreateError = '';
|
|
try {
|
|
const resp = await fetch(this.versioningHostMode ? '/api/host/workspaces/create' : '/api/projects/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({
|
|
path: this.versioningHostMode ? workspacePath : undefined,
|
|
label
|
|
})
|
|
});
|
|
const payload = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !payload?.success) {
|
|
throw new Error(payload?.error || (this.versioningHostMode ? '新建宿主机工作区失败' : '新建项目失败'));
|
|
}
|
|
await this.fetchHostWorkspaces();
|
|
const createdLabel = payload?.data?.workspace?.label || label || workspacePath;
|
|
this.hostWorkspaceCreateDialogOpen = false;
|
|
this.hostWorkspaceCreatePath = '';
|
|
this.hostWorkspaceCreateLabel = '';
|
|
this.uiPushToast({
|
|
title: this.versioningHostMode ? '工作区已创建' : '项目已创建',
|
|
message: createdLabel,
|
|
type: 'success'
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error || '新建失败');
|
|
this.hostWorkspaceCreateError = message;
|
|
this.uiPushToast({
|
|
title: this.versioningHostMode ? '新建工作区失败' : '新建项目失败',
|
|
message,
|
|
type: 'error'
|
|
});
|
|
} finally {
|
|
this.hostWorkspaceCreateSubmitting = false;
|
|
}
|
|
},
|
|
async submitHostWorkspaceCreateFromManage(payload = {}) {
|
|
this.hostWorkspaceCreatePath = String(payload?.path || '').trim();
|
|
this.hostWorkspaceCreateLabel = String(payload?.label || '').trim();
|
|
await this.submitHostWorkspaceCreate();
|
|
},
|
|
async renameHostWorkspace(payload = {}) {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
const workspaceId = String(payload?.workspace_id || '').trim();
|
|
const label = String(payload?.label || '').trim();
|
|
if (!workspaceId || !label) {
|
|
this.hostWorkspaceCreateError = this.dockerProjectMode ? '项目名称不能为空' : '工作区名称不能为空';
|
|
return;
|
|
}
|
|
this.hostWorkspaceManageSubmitting = true;
|
|
this.hostWorkspaceCreateError = '';
|
|
try {
|
|
const endpoint = this.versioningHostMode
|
|
? '/api/host/workspaces/rename'
|
|
: '/api/projects/rename';
|
|
const resp = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ workspace_id: workspaceId, label })
|
|
});
|
|
const result = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !result?.success) {
|
|
throw new Error(result?.error || '重命名失败');
|
|
}
|
|
await this.fetchHostWorkspaces();
|
|
this.uiPushToast({
|
|
title: this.versioningHostMode ? '工作区已重命名' : '项目已重命名',
|
|
message: label,
|
|
type: 'success'
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error || '重命名失败');
|
|
this.hostWorkspaceCreateError = message;
|
|
this.uiPushToast({
|
|
title: this.versioningHostMode ? '重命名工作区失败' : '重命名项目失败',
|
|
message,
|
|
type: 'error'
|
|
});
|
|
} finally {
|
|
this.hostWorkspaceManageSubmitting = false;
|
|
}
|
|
},
|
|
async deleteHostWorkspace(item) {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
const workspaceId = String(item?.workspace_id || '').trim();
|
|
if (!workspaceId) return;
|
|
const kindLabel = this.versioningHostMode ? '工作区' : '项目';
|
|
const wasCurrent = workspaceId === this.currentHostWorkspaceId;
|
|
const ok = await this.confirmAction({
|
|
title: `删除${kindLabel}`,
|
|
message: this.versioningHostMode
|
|
? `确定要从列表中删除“${item?.label || workspaceId}”吗?不会删除磁盘上的工作区文件夹。`
|
|
: `确定要删除项目“${item?.label || workspaceId}”吗?该项目文件夹和对话记录会被一并删除。`,
|
|
confirmText: '删除',
|
|
cancelText: '取消',
|
|
confirmVariant: 'danger'
|
|
});
|
|
if (!ok) return;
|
|
|
|
this.hostWorkspaceManageSubmitting = true;
|
|
this.hostWorkspaceCreateError = '';
|
|
try {
|
|
const endpoint = this.versioningHostMode
|
|
? '/api/host/workspaces/delete'
|
|
: '/api/projects/delete';
|
|
const resp = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ workspace_id: workspaceId })
|
|
});
|
|
const result = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !result?.success) {
|
|
throw new Error(result?.error || '删除失败');
|
|
}
|
|
const data = result.data || {};
|
|
this.currentHostWorkspaceId = String(data.current_workspace_id || this.currentHostWorkspaceId || '');
|
|
if (data.default_workspace_id) {
|
|
this.defaultHostWorkspaceId = String(data.default_workspace_id);
|
|
}
|
|
await this.fetchHostWorkspaces();
|
|
if (wasCurrent) {
|
|
this.messages = [];
|
|
this.currentConversationId = null;
|
|
this.currentConversationTitle = '新对话';
|
|
this.searchActive = false;
|
|
this.searchResults = [];
|
|
this.conversations = [];
|
|
this.conversationsOffset = 0;
|
|
this.hasMoreConversations = false;
|
|
this.conversationsLoading = true;
|
|
history.replaceState({}, '', '/new');
|
|
await this.loadConversationsList();
|
|
}
|
|
this.uiPushToast({
|
|
title: `${kindLabel}已删除`,
|
|
message: item?.label || workspaceId,
|
|
type: 'success'
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error || '删除失败');
|
|
this.hostWorkspaceCreateError = message;
|
|
this.uiPushToast({
|
|
title: `删除${kindLabel}失败`,
|
|
message,
|
|
type: 'error'
|
|
});
|
|
} finally {
|
|
this.hostWorkspaceManageSubmitting = false;
|
|
}
|
|
},
|
|
async setDefaultHostWorkspace(payload = {}) {
|
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
return;
|
|
}
|
|
const workspaceId = String(payload?.workspace_id || '').trim();
|
|
if (!workspaceId) return;
|
|
if (workspaceId === this.defaultHostWorkspaceId) return;
|
|
|
|
this.hostWorkspaceManageSubmitting = true;
|
|
this.hostWorkspaceCreateError = '';
|
|
const kindLabel = this.versioningHostMode ? '工作区' : '项目';
|
|
try {
|
|
const endpoint = this.versioningHostMode
|
|
? '/api/host/workspaces/set-default'
|
|
: '/api/projects/set-default';
|
|
const resp = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({ workspace_id: workspaceId })
|
|
});
|
|
const result = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !result?.success) {
|
|
throw new Error(result?.error || `设置默认${kindLabel}失败`);
|
|
}
|
|
const data = result.data || {};
|
|
this.defaultHostWorkspaceId = String(data.default_workspace_id || workspaceId);
|
|
await this.fetchHostWorkspaces();
|
|
this.uiPushToast({
|
|
title: `已设为默认${kindLabel}`,
|
|
message: (this.hostWorkspaces || []).find((w: any) => w.workspace_id === workspaceId)?.label || workspaceId,
|
|
type: 'success'
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error || `设置默认${kindLabel}失败`);
|
|
this.hostWorkspaceCreateError = message;
|
|
this.uiPushToast({
|
|
title: `设置默认${kindLabel}失败`,
|
|
message,
|
|
type: 'error'
|
|
});
|
|
} finally {
|
|
this.hostWorkspaceManageSubmitting = false;
|
|
}
|
|
}
|
|
};
|