agent-Specialization/static/src/app.ts

2183 lines
92 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
// static/app-enhanced.js - 修复版正确实现Token实时更新
import katex from 'katex';
import { mapActions, mapState, mapWritableState } from 'pinia';
import ChatArea from './components/chat/ChatArea.vue';
import ConversationSidebar from './components/sidebar/ConversationSidebar.vue';
import LeftPanel from './components/panels/LeftPanel.vue';
import FocusPanel from './components/panels/FocusPanel.vue';
import TokenDrawer from './components/token/TokenDrawer.vue';
import PersonalizationDrawer from './components/personalization/PersonalizationDrawer.vue';
import QuickMenu from './components/input/QuickMenu.vue';
import InputComposer from './components/input/InputComposer.vue';
import AppShell from './components/shell/AppShell.vue';
import { useUiStore } from './stores/ui';
import { useConversationStore } from './stores/conversation';
import { useChatStore } from './stores/chat';
import { useInputStore } from './stores/input';
import { useToolStore } from './stores/tool';
import { useResourceStore } from './stores/resource';
import { useUploadStore } from './stores/upload';
import { useFileStore } from './stores/file';
import { useSubAgentStore } from './stores/subAgent';
import { useFocusStore } from './stores/focus';
import { usePersonalizationStore } from './stores/personalization';
import { useChatActionStore } from './stores/chatActions';
import { ICONS, TOOL_CATEGORY_ICON_MAP } from './utils/icons';
import { initializeLegacySocket } from './composables/useLegacySocket';
import { useConnectionStore } from './stores/connection';
import { useEasterEgg } from './composables/useEasterEgg';
import { renderMarkdown as renderMarkdownHelper } from './composables/useMarkdownRenderer';
import {
formatTokenCount,
formatBytes,
formatPercentage,
formatRate,
formatResetTime,
formatQuotaValue,
quotaTypeLabel,
buildQuotaResetSummary,
isQuotaExceeded as isQuotaExceededUtil,
buildQuotaToastMessage
} from './utils/formatters';
import {
getToolIcon,
getToolAnimationClass,
getToolStatusText,
getToolDescription,
cloneToolArguments,
buildToolLabel,
formatSearchTopic,
formatSearchTime,
getLanguageClass
} from './utils/chatDisplay';
import {
scrollToBottom as scrollToBottomHelper,
conditionalScrollToBottom as conditionalScrollToBottomHelper,
toggleScrollLock as toggleScrollLockHelper,
scrollThinkingToBottom as scrollThinkingToBottomHelper
} from './composables/useScrollControl';
import {
startResize as startPanelResize,
handleResize as handlePanelResize,
stopResize as stopPanelResize
} from './composables/usePanelResize';
window.katex = katex;
function updateViewportHeightVar() {
const docEl = document.documentElement;
const visualViewport = window.visualViewport;
if (visualViewport) {
const vh = visualViewport.height;
const bottomInset = Math.max(
0,
(window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop
);
docEl.style.setProperty('--app-viewport', `${vh}px`);
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);
} else {
const height = window.innerHeight || docEl.clientHeight;
if (height) {
docEl.style.setProperty('--app-viewport', `${height}px`);
}
docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)');
}
}
updateViewportHeightVar();
window.addEventListener('resize', updateViewportHeightVar);
window.addEventListener('orientationchange', updateViewportHeightVar);
window.addEventListener('pageshow', updateViewportHeightVar);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', updateViewportHeightVar);
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
}
const appOptions = {
data() {
return {
// 路由相关
initialRouteResolved: false,
// 工具状态跟踪
preparingTools: new Map(),
activeTools: new Map(),
toolActionIndex: new Map(),
toolStacks: new Map(),
// ==========================================
// 对话管理相关状态
// ==========================================
// 搜索功能
// ==========================================
// Token统计相关状态修复版
// ==========================================
// 对话压缩状态
compressing: false,
skipConversationLoadedEvent: false,
skipConversationHistoryReload: false,
_scrollListenerReady: false,
historyLoading: false,
// 工具控制菜单
icons: ICONS,
toolCategoryIcons: TOOL_CATEGORY_ICON_MAP
}
},
created() {
const actionStore = useChatActionStore();
actionStore.registerDependencies({
pushToast: (payload) => this.uiPushToast(payload),
autoResizeInput: () => this.autoResizeInput(),
focusComposer: () => {
const inputEl = this.getComposerElement('stadiumInput');
if (inputEl && typeof inputEl.focus === 'function') {
inputEl.focus();
}
},
isConnected: () => this.isConnected,
getSocket: () => this.socket,
downloadResource: (url, filename) => this.downloadResource(url, filename)
});
},
async mounted() {
console.log('Vue应用已挂载');
if (window.ensureCsrfToken) {
window.ensureCsrfToken().catch((err) => {
console.warn('CSRF token 初始化失败:', err);
});
}
await this.bootstrapRoute();
await this.initSocket();
this.$nextTick(() => {
this.ensureScrollListener();
});
// 延迟加载初始数据
setTimeout(() => {
this.loadInitialData();
}, 500);
document.addEventListener('click', this.handleClickOutsideQuickMenu);
document.addEventListener('click', this.handleClickOutsidePanelMenu);
window.addEventListener('popstate', this.handlePopState);
this.subAgentFetch();
this.subAgentStartPolling();
this.$nextTick(() => {
this.autoResizeInput();
});
this.resourceStartContainerStatsPolling();
this.resourceStartProjectStoragePolling();
this.resourceStartUsageQuotaPolling();
},
computed: {
...mapWritableState(useConnectionStore, [
'isConnected',
'socket',
'stopRequested',
'projectPath',
'agentVersion',
'thinkingMode'
]),
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
...mapWritableState(useUiStore, [
'sidebarCollapsed',
'utilityPanelHidden',
'panelMode',
'panelMenuOpen',
'leftWidth',
'rightWidth',
'rightCollapsed',
'isResizing',
'resizingPanel',
'minPanelWidth',
'maxPanelWidth',
'quotaToast',
'toastQueue',
'confirmDialog',
'easterEgg'
]),
...mapWritableState(useConversationStore, [
'conversations',
'conversationsLoading',
'hasMoreConversations',
'loadingMoreConversations',
'currentConversationId',
'currentConversationTitle',
'searchQuery',
'searchTimer',
'conversationsOffset',
'conversationsLimit'
]),
...mapWritableState(useChatStore, [
'messages',
'currentMessageIndex',
'streamingMessage',
'expandedBlocks',
'autoScrollEnabled',
'userScrolling',
'thinkingScrollLocks'
]),
...mapWritableState(useInputStore, [
'inputMessage',
'inputLineCount',
'inputIsMultiline',
'inputIsFocused',
'quickMenuOpen',
'toolMenuOpen',
'settingsOpen'
]),
...mapWritableState(useToolStore, [
'preparingTools',
'activeTools',
'toolActionIndex',
'toolStacks',
'toolSettings',
'toolSettingsLoading'
]),
...mapWritableState(useResourceStore, [
'tokenPanelCollapsed',
'currentContextTokens',
'currentConversationTokens',
'projectStorage',
'containerStatus',
'containerNetRate',
'usageQuota'
]),
...mapWritableState(useFocusStore, ['focusedFiles']),
...mapWritableState(useUploadStore, ['uploading'])
},
beforeUnmount() {
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
window.removeEventListener('popstate', this.handlePopState);
this.subAgentStopPolling();
this.resourceStopContainerStatsPolling();
this.resourceStopProjectStoragePolling();
this.resourceStopUsageQuotaPolling();
const cleanup = this.destroyEasterEggEffect(true);
if (cleanup && typeof cleanup.catch === 'function') {
cleanup.catch(() => {});
}
},
watch: {
inputMessage() {
this.autoResizeInput();
},
currentConversationId: {
immediate: false,
handler(newValue, oldValue) {
console.log('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
return;
}
if (this.skipConversationHistoryReload) {
this.skipConversationHistoryReload = false;
return;
}
if (oldValue && newValue === oldValue) {
return;
}
this.fetchAndDisplayHistory();
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
}
}
},
methods: {
ensureScrollListener() {
if (this._scrollListenerReady) {
return;
}
const area = this.getMessagesAreaElement();
if (!area) {
return;
}
this.initScrollListener();
this._scrollListenerReady = true;
},
...mapActions(useUiStore, {
uiToggleSidebar: 'toggleSidebar',
uiSetPanelMode: 'setPanelMode',
uiSetPanelMenuOpen: 'setPanelMenuOpen',
uiTogglePanelMenu: 'togglePanelMenu',
uiToggleUtilityPanelHidden: 'toggleUtilityPanelHidden',
uiSetUtilityPanelHidden: 'setUtilityPanelHidden',
uiPushToast: 'pushToast',
uiUpdateToast: 'updateToast',
uiDismissToast: 'dismissToast',
uiShowQuotaToastMessage: 'showQuotaToastMessage',
uiDismissQuotaToast: 'dismissQuotaToast',
uiRequestConfirm: 'requestConfirm',
uiResolveConfirm: 'resolveConfirm'
}),
...mapActions(useChatStore, {
chatExpandBlock: 'expandBlock',
chatCollapseBlock: 'collapseBlock',
chatClearExpandedBlocks: 'clearExpandedBlocks',
chatSetThinkingLock: 'setThinkingLock',
chatClearThinkingLocks: 'clearThinkingLocks',
chatSetScrollState: 'setScrollState',
chatEnableAutoScroll: 'enableAutoScroll',
chatDisableAutoScroll: 'disableAutoScroll',
chatToggleScrollLockState: 'toggleScrollLockState',
chatAddUserMessage: 'addUserMessage',
chatStartAssistantMessage: 'startAssistantMessage',
chatStartThinkingAction: 'startThinkingAction',
chatAppendThinkingChunk: 'appendThinkingChunk',
chatCompleteThinkingAction: 'completeThinking',
chatStartTextAction: 'startTextAction',
chatAppendTextChunk: 'appendTextChunk',
chatCompleteTextAction: 'completeText',
chatAddSystemMessage: 'addSystemMessage',
chatAddAppendPayloadAction: 'addAppendPayloadAction',
chatAddModifyPayloadAction: 'addModifyPayloadAction',
chatEnsureAssistantMessage: 'ensureAssistantMessage'
}),
...mapActions(useInputStore, {
inputSetFocused: 'setInputFocused',
inputToggleQuickMenu: 'toggleQuickMenu',
inputCloseMenus: 'closeMenus',
inputOpenQuickMenu: 'openQuickMenu',
inputSetQuickMenuOpen: 'setQuickMenuOpen',
inputToggleToolMenu: 'toggleToolMenu',
inputSetToolMenuOpen: 'setToolMenuOpen',
inputToggleSettingsMenu: 'toggleSettingsMenu',
inputSetSettingsOpen: 'setSettingsOpen',
inputSetMessage: 'setInputMessage',
inputClearMessage: 'clearInputMessage',
inputSetLineCount: 'setInputLineCount',
inputSetMultiline: 'setInputMultiline'
}),
...mapActions(useToolStore, {
toolRegisterAction: 'registerToolAction',
toolUnregisterAction: 'unregisterToolAction',
toolFindAction: 'findToolAction',
toolTrackAction: 'trackToolAction',
toolReleaseAction: 'releaseToolAction',
toolGetLatestAction: 'getLatestActiveToolAction',
toolResetTracking: 'resetToolTracking',
toolSetSettings: 'setToolSettings',
toolSetSettingsLoading: 'setToolSettingsLoading'
}),
...mapActions(useResourceStore, {
resourceUpdateCurrentContextTokens: 'updateCurrentContextTokens',
resourceFetchConversationTokenStatistics: 'fetchConversationTokenStatistics',
resourceToggleTokenPanel: 'toggleTokenPanel',
resourceApplyStatusSnapshot: 'applyStatusSnapshot',
resourceUpdateContainerStatus: 'updateContainerStatus',
resourceStartContainerStatsPolling: 'startContainerStatsPolling',
resourceStopContainerStatsPolling: 'stopContainerStatsPolling',
resourceStartProjectStoragePolling: 'startProjectStoragePolling',
resourceStopProjectStoragePolling: 'stopProjectStoragePolling',
resourceStartUsageQuotaPolling: 'startUsageQuotaPolling',
resourceStopUsageQuotaPolling: 'stopUsageQuotaPolling',
resourcePollContainerStats: 'pollContainerStats',
resourcePollProjectStorage: 'pollProjectStorage',
resourceFetchUsageQuota: 'fetchUsageQuota',
resourceResetTokenStatistics: 'resetTokenStatistics',
resourceSetUsageQuota: 'setUsageQuota'
}),
...mapActions(useUploadStore, {
uploadHandleSelected: 'handleSelectedFiles'
}),
...mapActions(useFileStore, {
fileFetchTree: 'fetchFileTree',
fileSetTreeFromResponse: 'setFileTreeFromResponse',
fileFetchTodoList: 'fetchTodoList',
fileSetTodoList: 'setTodoList',
fileHideContextMenu: 'hideContextMenu'
}),
...mapActions(useSubAgentStore, {
subAgentFetch: 'fetchSubAgents',
subAgentStartPolling: 'startPolling',
subAgentStopPolling: 'stopPolling'
}),
...mapActions(useFocusStore, {
focusFetchFiles: 'fetchFocusedFiles',
focusSetFiles: 'setFocusedFiles'
}),
...mapActions(usePersonalizationStore, {
personalizationOpenDrawer: 'openDrawer'
}),
getInputComposerRef() {
return this.$refs.inputComposer || null;
},
getComposerElement(field) {
const composer = this.getInputComposerRef();
const unwrap = (value: any) => {
if (!value) {
return null;
}
if (value instanceof HTMLElement) {
return value;
}
if (typeof value === 'object' && 'value' in value && !(value instanceof Window)) {
return value.value;
}
return value;
};
if (composer && composer[field]) {
return unwrap(composer[field]);
}
if (this.$refs && this.$refs[field]) {
return unwrap(this.$refs[field]);
}
return null;
},
getMessagesAreaElement() {
const ref = this.$refs.messagesArea;
if (!ref) {
return null;
}
if (ref instanceof HTMLElement) {
return ref;
}
if (ref.rootEl) {
return ref.rootEl.value || ref.rootEl;
}
if (ref.$el && ref.$el.querySelector) {
const el = ref.$el.querySelector('.messages-area');
if (el) {
return el;
}
}
return null;
},
getThinkingContentElement(blockId) {
const chatArea = this.$refs.messagesArea;
if (chatArea && typeof chatArea.getThinkingRef === 'function') {
const el = chatArea.getThinkingRef(blockId);
if (el) {
return el;
}
}
const refName = `thinkingContent-${blockId}`;
const elRef = this.$refs[refName];
if (Array.isArray(elRef)) {
return elRef[0] || null;
}
return elRef || null;
},
logMessageState(action, extra = {}) {
const count = Array.isArray(this.messages) ? this.messages.length : 'N/A';
console.log('[Messages]', {
action,
count,
conversationId: this.currentConversationId,
streaming: this.streamingMessage,
...extra
});
},
iconStyle(iconKey, size) {
const iconPath = this.icons ? this.icons[iconKey] : null;
if (!iconPath) {
return {};
}
const style = { '--icon-src': `url(${iconPath})` };
if (size) {
style['--icon-size'] = size;
}
return style;
},
toolCategoryIcon(categoryId) {
return this.toolCategoryIcons[categoryId] || 'settings';
},
openGuiFileManager() {
window.open('/file-manager', '_blank');
},
findMessageByAction(action) {
if (!action) {
return null;
}
for (const message of this.messages) {
if (!message.actions) {
continue;
}
if (message.actions.includes(action)) {
return message;
}
}
return null;
},
renderMarkdown(content, isStreaming = false) {
return renderMarkdownHelper(content, isStreaming);
},
hasContainerStats() {
return !!(
this.containerStatus &&
this.containerStatus.mode === 'docker' &&
this.containerStatus.stats
);
},
containerStatusClass() {
if (!this.containerStatus) {
return 'status-pill--host';
}
if (this.containerStatus.mode !== 'docker') {
return 'status-pill--host';
}
const rawStatus = (
this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status)
) || '';
const status = String(rawStatus).toLowerCase();
if (status.includes('running')) {
return 'status-pill--running';
}
if (status.includes('paused')) {
return 'status-pill--stopped';
}
if (status.includes('exited') || status.includes('dead')) {
return 'status-pill--stopped';
}
return 'status-pill--running';
},
containerStatusText() {
if (!this.containerStatus) {
return '未知';
}
if (this.containerStatus.mode !== 'docker') {
return '宿主机模式';
}
const rawStatus = (
this.containerStatus.state &&
(this.containerStatus.state.status || this.containerStatus.state.Status)
) || '';
const status = String(rawStatus).toLowerCase();
if (status.includes('running')) {
return '运行中';
}
if (status.includes('paused')) {
return '已暂停';
}
if (status.includes('exited') || status.includes('dead')) {
return '已停止';
}
return rawStatus || '容器模式';
},
formatTime(value) {
if (!value) {
return '未知时间';
}
let date;
if (typeof value === 'number') {
date = new Date(value);
} else if (typeof value === 'string') {
const parsed = Date.parse(value);
if (!Number.isNaN(parsed)) {
date = new Date(parsed);
} else {
const numeric = Number(value);
if (!Number.isNaN(numeric)) {
date = new Date(numeric);
}
}
} else if (value instanceof Date) {
date = value;
}
if (!date || Number.isNaN(date.getTime())) {
return String(value);
}
const now = Date.now();
const diff = now - date.getTime();
if (diff < 60000) {
return '刚刚';
}
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins} 分钟前`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} 小时前`;
}
const formatter = new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return formatter.format(date);
},
async bootstrapRoute() {
const path = window.location.pathname.replace(/^\/+/, '');
if (!path || path === 'new') {
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.initialRouteResolved = true;
return;
}
const convId = path.startsWith('conv_') ? path : `conv_${path}`;
try {
const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' });
const result = await resp.json();
if (result.success) {
this.currentConversationId = convId;
this.currentConversationTitle = result.title || '对话';
history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`);
} else {
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
}
} catch (error) {
console.warn('初始化路由失败:', error);
history.replaceState({}, '', '/new');
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
} finally {
this.initialRouteResolved = true;
}
},
handlePopState(event) {
const state = event.state || {};
const convId = state.conversationId;
if (!convId) {
this.currentConversationId = null;
this.currentConversationTitle = '新对话';
this.logMessageState('handlePopState:clear-messages-no-conversation');
this.messages = [];
this.logMessageState('handlePopState:after-clear-no-conversation');
this.resetAllStates('handlePopState:no-conversation');
this.resetTokenStatistics();
return;
}
this.loadConversation(convId);
},
stripConversationPrefix(conversationId) {
if (!conversationId) return '';
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
},
async downloadFile(path) {
if (!path) {
this.fileHideContextMenu();
return;
}
const url = `/api/download/file?path=${encodeURIComponent(path)}`;
const name = path.split('/').pop() || 'file';
await this.downloadResource(url, name);
},
async downloadFolder(path) {
if (!path) {
this.fileHideContextMenu();
return;
}
const url = `/api/download/folder?path=${encodeURIComponent(path)}`;
const segments = path.split('/').filter(Boolean);
const folderName = segments.length ? segments.pop() : 'folder';
await this.downloadResource(url, `${folderName}.zip`);
},
async downloadResource(url, filename) {
try {
const response = await fetch(url);
if (!response.ok) {
let message = response.statusText;
try {
const errorData = await response.json();
message = errorData.error || errorData.message || message;
} catch (err) {
message = await response.text();
}
this.uiPushToast({
title: '下载失败',
message: message || '无法完成下载',
type: 'error'
});
return;
}
const blob = await response.blob();
const downloadName = filename || 'download';
const link = document.createElement('a');
const href = URL.createObjectURL(blob);
link.href = href;
link.download = downloadName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
} catch (error) {
console.error('下载失败:', error);
this.uiPushToast({
title: '下载失败',
message: error.message || String(error),
type: 'error'
});
} finally {
this.fileHideContextMenu();
}
},
initScrollListener() {
const messagesArea = this.getMessagesAreaElement();
if (!messagesArea) {
console.warn('消息区域未找到');
return;
}
this._scrollListenerReady = true;
let isProgrammaticScroll = false;
const bottomThreshold = 12;
this._setScrollingFlag = (flag) => {
isProgrammaticScroll = !!flag;
};
messagesArea.addEventListener('scroll', () => {
if (isProgrammaticScroll) {
return;
}
const scrollTop = messagesArea.scrollTop;
const scrollHeight = messagesArea.scrollHeight;
const clientHeight = messagesArea.clientHeight;
const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold;
if (isAtBottom) {
this.chatSetScrollState({ userScrolling: false, autoScrollEnabled: true });
} else {
this.chatSetScrollState({ userScrolling: true, autoScrollEnabled: false });
}
});
},
async initSocket() {
await initializeLegacySocket(this);
},
cleanupStaleToolActions() {
this.messages.forEach(msg => {
if (!msg.actions) {
return;
}
msg.actions.forEach(action => {
if (action.type !== 'tool' || !action.tool) {
return;
}
if (['running', 'preparing'].includes(action.tool.status)) {
action.tool.status = 'stale';
action.tool.message = action.tool.message || '已被新的响应中断';
this.toolUnregisterAction(action);
}
});
});
this.preparingTools.clear();
this.toolActionIndex.clear();
},
// 完整重置所有状态
resetAllStates(reason = 'unspecified') {
console.log('重置所有前端状态', { reason, conversationId: this.currentConversationId });
this.logMessageState('resetAllStates:before-cleanup', { reason });
this.fileHideContextMenu();
// 重置消息和流状态
this.streamingMessage = false;
this.currentMessageIndex = -1;
this.stopRequested = false;
// 清理工具状态
this.toolResetTracking();
// 新增:将所有未完成的工具标记为已完成
this.messages.forEach(msg => {
if (msg.role === 'assistant' && msg.actions) {
msg.actions.forEach(action => {
if (action.type === 'tool' &&
(action.tool.status === 'preparing' || action.tool.status === 'running')) {
action.tool.status = 'completed';
}
});
}
});
// 重置滚动状态
this.chatEnableAutoScroll();
// 清理Markdown缓存
if (this.markdownCache) {
this.markdownCache.clear();
}
this.chatClearThinkingLocks();
// 强制更新视图
this.$forceUpdate();
this.inputSetSettingsOpen(false);
this.inputSetToolMenuOpen(false);
this.inputSetQuickMenuOpen(false);
this.inputSetLineCount(1);
this.inputSetMultiline(false);
this.inputClearMessage();
this.toolSetSettingsLoading(false);
this.toolSetSettings([]);
console.log('前端状态重置完成');
this._scrollListenerReady = false;
this.$nextTick(() => {
this.ensureScrollListener();
});
this.logMessageState('resetAllStates:after-cleanup', { reason });
},
// 重置Token统计
openRealtimeTerminal() {
const { protocol, hostname, port } = window.location;
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
window.open(target, '_blank');
},
resetTokenStatistics() {
this.resourceResetTokenStatistics();
},
async loadInitialData() {
try {
console.log('加载初始数据...');
await this.fileFetchTree();
await this.focusFetchFiles();
await this.fileFetchTodoList();
const statusResponse = await fetch('/api/status');
const statusData = await statusResponse.json();
this.projectPath = statusData.project_path || '';
this.agentVersion = statusData.version || this.agentVersion;
this.thinkingMode = !!statusData.thinking_mode;
this.applyStatusSnapshot(statusData);
await this.fetchUsageQuota();
// 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
if (statusConversationId) {
if (!this.currentConversationId) {
this.skipConversationHistoryReload = true;
this.currentConversationId = statusConversationId;
}
// 如果有当前对话尝试获取标题和Token统计
try {
const convResponse = await fetch(`/api/conversations/current`);
const convData = await convResponse.json();
if (convData.success && convData.data) {
this.currentConversationTitle = convData.data.title;
}
await this.fetchAndDisplayHistory();
// 获取当前对话的Token统计
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
} catch (e) {
console.warn('获取当前对话标题失败:', e);
}
}
await this.loadToolSettings(true);
console.log('初始数据加载完成');
} catch (error) {
console.error('加载初始数据失败:', error);
}
},
// ==========================================
// Token / 资源状态封装Pinia store
// ==========================================
async updateCurrentContextTokens() {
await this.resourceUpdateCurrentContextTokens(this.currentConversationId);
},
async fetchConversationTokenStatistics() {
await this.resourceFetchConversationTokenStatistics(this.currentConversationId);
},
toggleTokenPanel() {
this.resourceToggleTokenPanel();
},
applyStatusSnapshot(status) {
this.resourceApplyStatusSnapshot(status);
},
updateContainerStatus(status) {
this.resourceUpdateContainerStatus(status);
},
pollContainerStats() {
return this.resourcePollContainerStats();
},
startContainerStatsPolling() {
this.resourceStartContainerStatsPolling();
},
stopContainerStatsPolling() {
this.resourceStopContainerStatsPolling();
},
pollProjectStorage() {
return this.resourcePollProjectStorage();
},
startProjectStoragePolling() {
this.resourceStartProjectStoragePolling();
},
stopProjectStoragePolling() {
this.resourceStopProjectStoragePolling();
},
fetchUsageQuota() {
return this.resourceFetchUsageQuota();
},
startUsageQuotaPolling() {
this.resourceStartUsageQuotaPolling();
},
stopUsageQuotaPolling() {
this.resourceStopUsageQuotaPolling();
},
// ==========================================
// 对话管理核心功能
// ==========================================
async loadConversationsList() {
this.conversationsLoading = true;
try {
const response = await fetch(`/api/conversations?limit=${this.conversationsLimit}&offset=${this.conversationsOffset}`);
const data = await response.json();
if (data.success) {
if (this.conversationsOffset === 0) {
this.conversations = data.data.conversations;
} else {
this.conversations.push(...data.data.conversations);
}
if (this.currentConversationId) {
this.promoteConversationToTop(this.currentConversationId);
}
this.hasMoreConversations = data.data.has_more;
console.log(`已加载 ${this.conversations.length} 个对话`);
if (this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0) {
const latestConversation = this.conversations[0];
if (latestConversation && latestConversation.id) {
await this.loadConversation(latestConversation.id);
}
}
} else {
console.error('加载对话列表失败:', data.error);
}
} catch (error) {
console.error('加载对话列表异常:', error);
} finally {
this.conversationsLoading = false;
}
},
async loadMoreConversations() {
if (this.loadingMoreConversations || !this.hasMoreConversations) return;
this.loadingMoreConversations = true;
this.conversationsOffset += this.conversationsLimit;
await this.loadConversationsList();
this.loadingMoreConversations = false;
},
async loadConversation(conversationId) {
console.log('加载对话:', conversationId);
this.logMessageState('loadConversation:start', { conversationId });
if (conversationId === this.currentConversationId) {
console.log('已是当前对话,跳过加载');
return;
}
try {
// 1. 调用加载API
const response = await fetch(`/api/conversations/${conversationId}/load`, {
method: 'PUT'
});
const result = await response.json();
if (result.success) {
console.log('对话加载API成功:', result);
// 2. 更新当前对话信息
this.skipConversationHistoryReload = true;
this.currentConversationId = conversationId;
this.currentConversationTitle = result.title;
this.promoteConversationToTop(conversationId);
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`);
this.skipConversationLoadedEvent = true;
// 3. 重置UI状态
this.resetAllStates(`loadConversation:${conversationId}`);
this.subAgentFetch();
this.fetchTodoList();
// 4. 延迟获取并显示历史对话内容(关键功能)
setTimeout(() => {
this.fetchAndDisplayHistory();
}, 300);
// 5. 获取Token统计重点加载历史累计统计+当前上下文)
setTimeout(() => {
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
}, 500);
} else {
console.error('对话加载失败:', result.message);
this.uiPushToast({
title: '加载对话失败',
message: result.message || '服务器未返回成功状态',
type: 'error'
});
}
} catch (error) {
console.error('加载对话异常:', error);
this.uiPushToast({
title: '加载对话异常',
message: error.message || String(error),
type: 'error'
});
}
},
promoteConversationToTop(conversationId) {
if (!Array.isArray(this.conversations) || !conversationId) {
return;
}
const index = this.conversations.findIndex(conv => conv && conv.id === conversationId);
if (index > 0) {
const [selected] = this.conversations.splice(index, 1);
this.conversations.unshift(selected);
}
},
// ==========================================
// 关键功能:获取并显示历史对话内容
// ==========================================
async fetchAndDisplayHistory() {
if (this.historyLoading) {
console.log('历史消息正在加载,跳过重复请求');
return;
}
this.historyLoading = true;
try {
console.log('开始获取历史对话内容...');
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId });
if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) {
console.log('ID');
return;
}
try {
// 使用专门的API获取对话消息历史
const messagesResponse = await fetch(`/api/conversations/${this.currentConversationId}/messages`);
if (!messagesResponse.ok) {
console.warn('');
// 备用方案通过状态API获取
const statusResponse = await fetch('/api/status');
const status = await statusResponse.json();
console.log(':', status);
this.applyStatusSnapshot(status);
// 如果状态中有对话历史字段
if (status.conversation_history && Array.isArray(status.conversation_history)) {
this.renderHistoryMessages(status.conversation_history);
return;
}
console.log('');
return;
}
const messagesData = await messagesResponse.json();
console.log(':', messagesData);
if (messagesData.success && messagesData.data && messagesData.data.messages) {
const messages = messagesData.data.messages;
console.log(`发现 ${messages.length} 条历史消息`);
if (messages.length > 0) {
// 清空当前显示的消息
this.logMessageState('fetchAndDisplayHistory:before-clear-existing');
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:after-clear-existing');
// 渲染历史消息 - 这是关键功能
this.renderHistoryMessages(messages);
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
console.log('');
} else {
console.log('');
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
}
} else {
console.log(':', messagesData);
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
}
} catch (error) {
console.error(':', error);
console.log('');
// 不显示alert避免打断用户体验
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) });
this.messages = [];
this.logMessageState('fetchAndDisplayHistory:error-cleared');
}
} finally {
this.historyLoading = false;
}
},
// ==========================================
// 关键功能:渲染历史消息
// ==========================================
renderHistoryMessages(historyMessages) {
console.log('...', historyMessages);
console.log(':', historyMessages.length);
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
if (!Array.isArray(historyMessages)) {
console.error('');
return;
}
let currentAssistantMessage = null;
historyMessages.forEach((message, index) => {
console.log(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
if (message.role === 'user') {
// 用户消息 - 先结束之前的assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
currentAssistantMessage = null;
}
this.messages.push({
role: 'user',
content: message.content || ''
});
console.log(':', message.content?.substring(0, 50) + '...');
} else if (message.role === 'assistant') {
// AI消息 - 如果没有当前assistant消息创建一个
if (!currentAssistantMessage) {
currentAssistantMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null,
activeThinkingId: null
};
}
const content = message.content || '';
let reasoningText = (message.reasoning_content || '').trim();
if (!reasoningText) {
const thinkPatterns = [
/<think>([\s\S]*?)<\/think>/g,
/<thinking>([\s\S]*?)<\/thinking>/g
];
let extracted = '';
for (const pattern of thinkPatterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
extracted += (match[1] || '').trim() + '\n';
}
}
reasoningText = extracted.trim();
}
if (reasoningText) {
const blockId = `history-thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
currentAssistantMessage.actions.push({
id: `history-think-${Date.now()}-${Math.random()}`,
type: 'thinking',
content: reasoningText,
streaming: false,
timestamp: Date.now(),
blockId
});
console.log(':', reasoningText.substring(0, 50) + '...');
}
// 处理普通文本内容(移除思考标签后的内容)
const metadata = message.metadata || {};
const appendPayloadMeta = metadata.append_payload;
const modifyPayloadMeta = metadata.modify_payload;
const isAppendMessage = message.name === 'append_to_file';
const isModifyMessage = message.name === 'modify_file';
const containsAppendMarkers = /<<<\s*(APPEND|MODIFY)/i.test(content || '') || /<<<END_\s*(APPEND|MODIFY)>>>/i.test(content || '');
let textContent = content;
if (!message.reasoning_content) {
textContent = textContent
.replace(/<think>[\s\S]*?<\/think>/g, '')
.replace(/<thinking>[\s\S]*?<\/thinking>/g, '')
.trim();
} else {
textContent = textContent.trim();
}
if (appendPayloadMeta) {
currentAssistantMessage.actions.push({
id: `history-append-payload-${Date.now()}-${Math.random()}`,
type: 'append_payload',
append: {
path: appendPayloadMeta.path || '',
forced: !!appendPayloadMeta.forced,
success: appendPayloadMeta.success === undefined ? true : !!appendPayloadMeta.success,
lines: appendPayloadMeta.lines ?? null,
bytes: appendPayloadMeta.bytes ?? null
},
timestamp: Date.now()
});
console.log('append占位信息:', appendPayloadMeta.path);
} else if (modifyPayloadMeta) {
currentAssistantMessage.actions.push({
id: `history-modify-payload-${Date.now()}-${Math.random()}`,
type: 'modify_payload',
modify: {
path: modifyPayloadMeta.path || '',
total: modifyPayloadMeta.total_blocks ?? null,
completed: modifyPayloadMeta.completed || [],
failed: modifyPayloadMeta.failed || [],
forced: !!modifyPayloadMeta.forced,
details: modifyPayloadMeta.details || []
},
timestamp: Date.now()
});
console.log('modify占位信息:', modifyPayloadMeta.path);
}
if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) {
currentAssistantMessage.actions.push({
id: `history-text-${Date.now()}-${Math.random()}`,
type: 'text',
content: textContent,
streaming: false,
timestamp: Date.now()
});
console.log(':', textContent.substring(0, 50) + '...');
}
// 处理工具调用
if (message.tool_calls && Array.isArray(message.tool_calls)) {
message.tool_calls.forEach((toolCall, tcIndex) => {
let arguments_obj = {};
try {
arguments_obj = typeof toolCall.function.arguments === 'string'
? JSON.parse(toolCall.function.arguments || '{}')
: (toolCall.function.arguments || {});
} catch (e) {
console.warn(':', e);
arguments_obj = {};
}
currentAssistantMessage.actions.push({
id: `history-tool-${toolCall.id || Date.now()}-${tcIndex}`,
type: 'tool',
tool: {
id: toolCall.id,
name: toolCall.function.name,
arguments: arguments_obj,
status: 'preparing',
result: null
},
timestamp: Date.now()
});
console.log(':', toolCall.function.name);
});
}
} else if (message.role === 'tool') {
// 工具结果 - 更新当前assistant消息中对应的工具
if (currentAssistantMessage) {
// 查找对应的工具action - 使用更灵活的匹配
let toolAction = null;
// 优先按tool_call_id匹配
if (message.tool_call_id) {
toolAction = currentAssistantMessage.actions.find(action =>
action.type === 'tool' &&
action.tool.id === message.tool_call_id
);
}
// 如果找不到按name匹配最后一个同名工具
if (!toolAction && message.name) {
const sameNameTools = currentAssistantMessage.actions.filter(action =>
action.type === 'tool' &&
action.tool.name === message.name
);
toolAction = sameNameTools[sameNameTools.length - 1]; // 取最后一个
}
if (toolAction) {
// 解析工具结果
let result;
try {
// 尝试解析为JSON
result = JSON.parse(message.content);
} catch (e) {
// 如果不是JSON就作为纯文本
result = {
output: message.content,
success: true
};
}
toolAction.tool.status = 'completed';
toolAction.tool.result = result;
if (result && typeof result === 'object') {
if (result.error) {
toolAction.tool.message = result.error;
} else if (result.message && !toolAction.tool.message) {
toolAction.tool.message = result.message;
}
}
if (message.name === 'append_to_file' && result && result.message) {
toolAction.tool.message = result.message;
}
console.log(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
// append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复
} else {
console.warn(':', message.name, message.tool_call_id);
}
}
} else {
// 其他类型消息如system- 先结束当前assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
currentAssistantMessage = null;
}
console.log(':', message.role);
this.messages.push({
role: message.role,
content: message.content || ''
});
}
});
// 处理最后一个assistant消息
if (currentAssistantMessage && currentAssistantMessage.actions.length > 0) {
this.messages.push(currentAssistantMessage);
}
console.log(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
this.logMessageState('renderHistoryMessages:after-render');
// 强制更新视图
this.$forceUpdate();
// 确保滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
setTimeout(() => {
const blockCount = this.$el && this.$el.querySelectorAll
? this.$el.querySelectorAll('.message-block').length
: 'N/A';
console.log('[Messages] DOM ', {
blocks: blockCount,
conversationId: this.currentConversationId
});
}, 0);
});
},
async createNewConversation() {
console.log('...');
this.logMessageState('createNewConversation:start');
try {
const response = await fetch('/api/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
thinking_mode: this.thinkingMode
})
});
const result = await response.json();
if (result.success) {
console.log(':', result.conversation_id);
// 清空当前消息
this.logMessageState('createNewConversation:before-clear');
this.messages = [];
this.logMessageState('createNewConversation:after-clear');
this.currentConversationId = result.conversation_id;
this.currentConversationTitle = '';
history.pushState({ conversationId: this.currentConversationId }, '', `/${this.stripConversationPrefix(this.currentConversationId)}`);
// 重置Token统计
this.resetTokenStatistics();
// 重置状态
this.resetAllStates('createNewConversation');
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
console.error(':', result.message);
this.uiPushToast({
title: '',
message: result.message || '',
type: 'error'
});
}
} catch (error) {
console.error(':', error);
this.uiPushToast({
title: '',
message: error.message || String(error),
type: 'error'
});
}
},
async deleteConversation(conversationId) {
const confirmed = await this.confirmAction({
title: '',
message: '',
confirmText: '',
cancelText: ''
});
if (!confirmed) {
return;
}
console.log(':', conversationId);
this.logMessageState('deleteConversation:start', { conversationId });
try {
const response = await fetch(`/api/conversations/${conversationId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
console.log('');
// 如果删除的是当前对话,清空界面
if (conversationId === this.currentConversationId) {
this.logMessageState('deleteConversation:before-clear', { conversationId });
this.messages = [];
this.logMessageState('deleteConversation:after-clear', { conversationId });
this.currentConversationId = null;
this.currentConversationTitle = '';
this.resetAllStates(`deleteConversation:${conversationId}`);
this.resetTokenStatistics();
history.replaceState({}, '', '/new');
}
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
console.error(':', result.message);
this.uiPushToast({
title: '',
message: result.message || '',
type: 'error'
});
}
} catch (error) {
console.error(':', error);
this.uiPushToast({
title: '',
message: error.message || String(error),
type: 'error'
});
}
},
async duplicateConversation(conversationId) {
console.log(':', conversationId);
try {
const response = await fetch(`/api/conversations/${conversationId}/duplicate`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
const newId = result.duplicate_conversation_id;
if (newId) {
this.currentConversationId = newId;
}
this.conversationsOffset = 0;
await this.loadConversationsList();
} else {
const message = result.message || result.error || '';
this.uiPushToast({
title: '',
message,
type: 'error'
});
}
} catch (error) {
console.error(':', error);
this.uiPushToast({
title: '',
message: error.message || String(error),
type: 'error'
});
}
},
searchConversations() {
// 简单的搜索功能实际实现可以调用搜索API
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
this.searchTimer = setTimeout(() => {
if (this.searchQuery.trim()) {
console.log(':', this.searchQuery);
// TODO: 实现搜索API调用
// this.searchConversationsAPI(this.searchQuery);
} else {
// 清空搜索,重新加载全部对话
this.conversationsOffset = 0;
this.loadConversationsList();
}
}, 300);
},
handleSidebarSearch(value) {
this.searchQuery = value;
this.searchConversations();
},
toggleSidebar() {
this.uiToggleSidebar();
},
toggleUtilityPanelVisibility() {
this.uiToggleUtilityPanelHidden();
},
togglePanelMenu() {
this.uiTogglePanelMenu();
},
selectPanelMode(mode) {
this.uiSetPanelMode(mode);
this.uiSetPanelMenuOpen(false);
},
openPersonalPage() {
this.personalizationOpenDrawer();
},
fetchTodoList() {
return this.fileFetchTodoList();
},
fetchSubAgents() {
return this.subAgentFetch();
},
async toggleThinkingMode() {
const nextMode = !this.thinkingMode;
try {
const response = await fetch('/api/thinking-mode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ thinking_mode: nextMode })
});
const data = await response.json();
if (response.ok && data.success) {
const actual = typeof data.data === 'boolean' ? data.data : nextMode;
this.thinkingMode = actual;
return;
}
throw new Error(data.message || data.error || '');
} catch (error) {
console.error(':', error);
this.uiPushToast({
title: '',
message: error.message || '',
type: 'error'
});
}
},
triggerFileUpload() {
if (this.uploading) {
return;
}
const input = this.getComposerElement('fileUploadInput');
if (input) {
input.click();
}
},
handleFileSelected(files) {
this.uploadHandleSelected(files);
},
handleSendOrStop() {
if (this.streamingMessage) {
this.stopTask();
} else {
this.sendMessage();
}
},
sendMessage() {
if (this.streamingMessage || !this.isConnected) {
return;
}
if (!this.inputMessage.trim()) {
return;
}
const quotaType = this.thinkingMode ? 'thinking' : 'fast';
if (this.isQuotaExceeded(quotaType)) {
this.showQuotaToast({ type: quotaType });
return;
}
const message = this.inputMessage;
if (message.startsWith('/')) {
this.socket.emit('send_command', { command: message });
this.inputClearMessage();
this.autoResizeInput();
return;
}
this.chatAddUserMessage(message);
this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId });
this.inputClearMessage();
this.inputSetLineCount(1);
this.inputSetMultiline(false);
this.chatEnableAutoScroll();
this.scrollToBottom();
this.autoResizeInput();
// 发送消息后延迟更新当前上下文Token关键修复恢复原逻辑
setTimeout(() => {
if (this.currentConversationId) {
this.updateCurrentContextTokens();
}
}, 1000);
},
// 新增:停止任务方法
stopTask() {
if (this.streamingMessage && !this.stopRequested) {
this.socket.emit('stop_task');
this.stopRequested = true;
console.log('');
}
},
async clearChat() {
const confirmed = await this.confirmAction({
title: '',
message: '',
confirmText: '',
cancelText: ''
});
if (confirmed) {
this.socket.emit('send_command', { command: '/clear' });
}
},
async compressConversation() {
if (!this.currentConversationId) {
this.uiPushToast({
title: '',
message: '',
type: 'info'
});
return;
}
if (this.compressing) {
return;
}
const confirmed = await this.confirmAction({
title: '',
message: '',
confirmText: '',
cancelText: ''
});
if (!confirmed) {
return;
}
this.compressing = true;
try {
const response = await fetch(`/api/conversations/${this.currentConversationId}/compress`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.success) {
const newId = result.compressed_conversation_id;
if (newId) {
this.currentConversationId = newId;
}
console.log(':', result);
} else {
const message = result.message || result.error || '';
this.uiPushToast({
title: '',
message,
type: 'error'
});
}
} catch (error) {
console.error(':', error);
this.uiPushToast({
title: '',
message: error.message || '',
type: 'error'
});
} finally {
this.compressing = false;
}
},
toggleToolMenu() {
if (!this.isConnected) {
return;
}
const nextState = this.inputToggleToolMenu();
if (nextState) {
this.inputSetSettingsOpen(false);
if (!this.quickMenuOpen) {
this.inputOpenQuickMenu();
}
this.loadToolSettings(true);
} else {
this.inputSetToolMenuOpen(false);
}
},
toggleQuickMenu() {
if (!this.isConnected) {
return;
}
this.inputToggleQuickMenu();
},
closeQuickMenu() {
this.inputCloseMenus();
},
handleQuickUpload() {
if (this.uploading || !this.isConnected) {
return;
}
this.triggerFileUpload();
},
handleQuickModeToggle() {
if (!this.isConnected || this.streamingMessage) {
return;
}
this.toggleThinkingMode();
},
handleInputChange() {
this.autoResizeInput();
},
handleInputFocus() {
this.inputSetFocused(true);
this.closeQuickMenu();
},
handleInputBlur() {
this.inputSetFocused(false);
},
handleRealtimeTerminalClick() {
if (!this.isConnected) {
return;
}
this.openRealtimeTerminal();
},
handleFocusPanelToggleClick() {
if (!this.isConnected) {
return;
}
this.toggleFocusPanel();
},
handleTokenPanelToggleClick() {
if (!this.currentConversationId) {
return;
}
this.toggleTokenPanel();
},
handleCompressConversationClick() {
if (this.compressing || this.streamingMessage || !this.isConnected) {
return;
}
this.compressConversation();
},
autoResizeInput() {
this.$nextTick(() => {
const textarea = this.getComposerElement('stadiumInput');
if (!textarea || !(textarea instanceof HTMLTextAreaElement)) {
return;
}
const previousHeight = textarea.offsetHeight;
textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6;
const targetHeight = Math.min(textarea.scrollHeight, maxHeight);
this.inputSetLineCount(Math.max(1, Math.round(targetHeight / lineHeight)));
this.inputSetMultiline(targetHeight > lineHeight * 1.4);
if (Math.abs(targetHeight - previousHeight) <= 0.5) {
textarea.style.height = `${targetHeight}px`;
return;
}
textarea.style.height = `${previousHeight}px`;
void textarea.offsetHeight;
requestAnimationFrame(() => {
textarea.style.height = `${targetHeight}px`;
});
});
},
handleClickOutsideQuickMenu(event) {
if (!this.quickMenuOpen) {
return;
}
const shell = this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell');
if (shell && shell.contains(event.target)) {
return;
}
this.closeQuickMenu();
},
handleClickOutsidePanelMenu(event) {
if (!this.panelMenuOpen) {
return;
}
const leftPanel = this.$refs.leftPanel;
const wrapper = leftPanel && leftPanel.panelMenuWrapper ? leftPanel.panelMenuWrapper : null;
if (wrapper && wrapper.contains(event.target)) {
return;
}
this.uiSetPanelMenuOpen(false);
},
applyToolSettingsSnapshot(categories) {
if (!Array.isArray(categories)) {
console.warn('[ToolSettings] Snapshot skipped: categories not array', categories);
return;
}
const normalized = categories.map((item) => ({
id: item.id,
label: item.label || item.id,
enabled: !!item.enabled,
tools: Array.isArray(item.tools) ? item.tools : []
}));
console.log('[ToolSettings] Snapshot applied', {
received: categories.length,
normalized,
anyEnabled: normalized.some(cat => cat.enabled),
toolExamples: normalized.slice(0, 3)
});
this.toolSetSettings(normalized);
this.toolSetSettingsLoading(false);
},
async loadToolSettings(force = false) {
if (!this.isConnected && !force) {
console.log('[ToolSettings] Skip load: disconnected & not forced');
return;
}
if (this.toolSettingsLoading) {
console.log('[ToolSettings] Skip load: already loading');
return;
}
if (!force && this.toolSettings.length > 0) {
console.log('[ToolSettings] Skip load: already have settings');
return;
}
console.log('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected });
this.toolSetSettingsLoading(true);
try {
const response = await fetch('/api/tool-settings');
const data = await response.json();
console.log('[ToolSettings] Fetch response', { status: response.status, data });
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn(':', data);
this.toolSetSettingsLoading(false);
}
} catch (error) {
console.error(':', error);
this.toolSetSettingsLoading(false);
}
},
async updateToolCategory(categoryId, enabled) {
if (!this.isConnected) {
return;
}
if (this.toolSettingsLoading) {
return;
}
const previousSnapshot = this.toolSettings.map((item) => ({ ...item }));
const updatedSettings = this.toolSettings.map((item) => {
if (item.id === categoryId) {
return { ...item, enabled };
}
return item;
});
this.toolSetSettings(updatedSettings);
try {
const response = await fetch('/api/tool-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
category: categoryId,
enabled
})
});
const data = await response.json();
if (response.ok && data.success && Array.isArray(data.categories)) {
this.applyToolSettingsSnapshot(data.categories);
} else {
console.warn(':', data);
this.toolSetSettings(previousSnapshot);
}
} catch (error) {
console.error(':', error);
this.toolSetSettings(previousSnapshot);
}
this.toolSetSettingsLoading(false);
},
toggleSettings() {
if (!this.isConnected) {
return;
}
const nextState = this.inputToggleSettingsMenu();
if (nextState) {
this.inputSetToolMenuOpen(false);
if (!this.quickMenuOpen) {
this.inputOpenQuickMenu();
}
}
},
toggleFocusPanel() {
this.rightCollapsed = !this.rightCollapsed;
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
this.rightWidth = this.minPanelWidth;
}
},
addSystemMessage(content) {
this.chatAddSystemMessage(content);
this.$forceUpdate();
this.conditionalScrollToBottom();
},
toggleBlock(blockId) {
if (!blockId) {
return;
}
if (this.expandedBlocks && this.expandedBlocks.has(blockId)) {
this.chatCollapseBlock(blockId);
} else {
this.chatExpandBlock(blockId);
}
},
handleThinkingScroll(blockId, event) {
if (!blockId || !event || !event.target) {
return;
}
const el = event.target;
const threshold = 12;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
this.chatSetThinkingLock(blockId, atBottom);
},
getToolIcon,
getToolAnimationClass,
getToolStatusText,
getToolDescription,
cloneToolArguments,
buildToolLabel,
formatSearchTopic,
formatSearchTime,
getLanguageClass,
scrollToBottom() {
scrollToBottomHelper(this);
},
conditionalScrollToBottom() {
conditionalScrollToBottomHelper(this);
},
toggleScrollLock() {
toggleScrollLockHelper(this);
},
scrollThinkingToBottom(blockId) {
scrollThinkingToBottomHelper(this, blockId);
},
// 面板调整方法
startResize(panel, event) {
startPanelResize(this, panel, event);
},
handleResize(event) {
handlePanelResize(this, event);
},
stopResize() {
stopPanelResize(this);
},
async handleEasterEggPayload(payload) {
const controller = useEasterEgg();
await controller.handlePayload(payload, this);
},
async startEasterEggEffect(effectName, payload = {}) {
const controller = useEasterEgg();
await controller.startEffect(effectName, payload, this);
},
destroyEasterEggEffect(forceImmediate = false) {
const controller = useEasterEgg();
return controller.destroyEffect(forceImmediate);
},
finishEasterEggCleanup() {
const controller = useEasterEgg();
controller.finishCleanup();
},
formatTokenCount,
formatBytes,
formatPercentage,
formatRate,
quotaTypeLabel,
formatResetTime,
formatQuotaValue,
quotaResetSummary() {
return buildQuotaResetSummary(this.usageQuota);
},
isQuotaExceeded(type) {
return isQuotaExceededUtil(this.usageQuota, type);
},
showQuotaToast(payload) {
if (!payload) {
return;
}
const type = payload.type || 'fast';
const message = buildQuotaToastMessage(type, this.usageQuota, payload.reset_at);
this.uiShowQuotaToastMessage(message, type);
},
confirmAction(options = {}) {
return this.uiRequestConfirm(options);
}
}
};
(appOptions as any).components = {
ChatArea,
ConversationSidebar,
LeftPanel,
FocusPanel,
TokenDrawer,
PersonalizationDrawer,
QuickMenu,
InputComposer,
AppShell
};
export default appOptions;