agent-Specialization/static/src/app/methods/ui/hostWorkspace.ts
JOJO f661dc6456 refactor(server,modules,utils,frontend): split oversized modules into packages and fix regressions
- 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
2026-06-20 15:54:48 +08:00

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;
}
}
};