agent-Specialization/static/src/App.vue
JOJO 8a1e17b9a0 feat(goal): add autonomous goal mode workflow
Introduce workspace-level goal state persistence, goal prompt injection, and after-turn review handling so an active task can continue until the configured completion conditions are met.

Add a dedicated goal review agent with readonly and active evidence modes, configurable model settings, review prompt, token/turn boundaries, idle-no-tool protection, and progress/completed/stopped events.

Wire goal_mode through task creation, task restoration, compression handoff, runtime user messages, API message sanitization, and tool-call ordering so goal continuations survive long-running tasks and deep compression.

Add Vue UI for arming goal mode from the quick menu, showing running/completed banners, displaying progress metrics, restoring running goal state, and exposing personalization settings for review mode and stop limits.

Include goal mode research notes and default goal review configuration.
2026-05-30 12:51:42 +08:00

825 lines
34 KiB
Vue
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.

<template>
<AppShell :download-file="downloadFile" :download-folder="downloadFolder">
<!-- 拖拽上传提示层 -->
<transition name="drag-overlay-fade">
<div v-if="dragOverActive" class="drag-upload-overlay">
<div class="drag-upload-content">
<div class="drag-upload-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
</div>
<div class="drag-upload-text">松开鼠标上传图片</div>
<div class="drag-upload-hint">支持拖拽图片、视频文件到此处</div>
</div>
</div>
</transition>
<div v-if="!isConnected && messages.length === 0" class="app-loading-state">
<div class="loading-animation" aria-hidden="true">
<!-- From Uiverse.io by Nawsome -->
<div class="boxes">
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
<div class="loading-copy">
<h2>加载中...</h2>
<p>正在连接服务器,请稍候</p>
<button type="button" class="loading-refresh-btn" @click="refreshCurrentPage">
刷新页面
</button>
</div>
</div>
<template v-else>
<div class="main-container">
<ConversationSidebar
v-if="!isMobileViewport"
:icon-style="iconStyle"
:format-time="formatTime"
:running-tasks="runningWorkspaceTasks"
:current-workspace-id="currentHostWorkspaceId"
:display-mode="chatDisplayMode"
:display-mode-disabled="displayModeSwitchDisabled"
@toggle="toggleSidebar"
@create="createNewConversation"
@search="handleSidebarSearchInput"
@search-submit="handleSidebarSearchSubmit"
@search-more="loadMoreSearchResults"
@select="loadConversation"
@select-running-task="openRunningWorkspaceTask"
@load-more="loadMoreConversations"
@personal="openPersonalPage"
@delete="deleteConversation"
@duplicate="duplicateConversation"
@toggle-workspace="handleWorkspaceToggle"
@toggle-display-mode="handleDisplayModeToggle"
/>
<div v-if="!isMobileViewport" class="workspace-region">
<LeftPanel
ref="leftPanel"
class="workspace-panel"
:class="{ 'workspace-panel--collapsed': workspaceCollapsed }"
:width="leftWidth"
:collapsed="workspaceCollapsed"
:icon-style="iconStyle"
:agent-version="agentVersion"
:thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:is-connected="isConnected"
:panel-menu-open="panelMenuOpen"
:panel-mode="panelMode"
:file-manager-disabled="policyUiBlocks.block_file_manager"
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
:host-workspaces="hostWorkspaces"
:host-workspace-id="currentHostWorkspaceId"
:host-workspace-default-id="defaultHostWorkspaceId"
:host-workspace-switching="hostWorkspaceSwitching"
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
@toggle-thinking-mode="handleQuickModeToggle"
@switch-host-workspace="handleHostWorkspaceSwitch"
@create-host-workspace="handleCreateHostWorkspace"
@manage-host-workspace="openHostWorkspaceManageDialog"
/>
<div
v-if="!workspaceCollapsed"
class="resize-handle"
@mousedown="startResize('left', $event)"
></div>
</div>
<main
class="chat-container"
:class="{
'chat-container--immersive': workspaceCollapsed,
'chat-container--mobile': isMobileViewport,
'chat-container--monitor': chatDisplayMode === 'monitor',
'has-title-ribbon': titleRibbonVisible
}"
:style="chatContainerStyle"
>
<TokenDrawer
:visible="Boolean(currentConversationId)"
:collapsed="tokenPanelCollapsed"
@toggle="handleTokenPanelToggleClick"
:current-conversation-tokens="currentConversationTokens"
:current-context-tokens="currentContextTokens"
:container-status="containerStatus"
:container-net-rate="containerNetRate"
:project-storage="projectStorage"
:usage-quota="usageQuota"
:format-token-count="formatTokenCount"
:format-percentage="formatPercentage"
:format-bytes="formatBytes"
:format-quota-value="formatQuotaValue"
:format-reset-time="formatResetTime"
:format-rate="formatRate"
/>
<div v-if="titleRibbonVisible" class="conversation-ribbon" ref="titleRibbon">
<button
type="button"
class="conversation-ribbon__selector"
data-tutorial="header-model-selector"
:class="{ open: headerMenuOpen }"
@click.stop="toggleHeaderMenu"
:disabled="!isConnected"
>
<span class="selector-label">
<span class="selector-model">{{ currentModelLabel }}</span>
<span class="selector-sep">·</span>
<span class="selector-mode">{{ headerRunModeLabel }}</span>
</span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="selector-caret">
<path
d="M7 10l5 5 5-5"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<span class="conversation-ribbon__text">{{
titleTypingText || currentConversationTitle
}}</span>
<transition name="header-menu">
<div v-if="headerMenuOpen" class="model-mode-dropdown" ref="headerMenu">
<div class="dropdown-column" data-tutorial="header-model-options">
<div class="dropdown-title">模型</div>
<div class="dropdown-list dropdown-list--models">
<button
v-for="option in modelOptions"
:key="option.key"
type="button"
class="dropdown-item"
:class="{ active: option.key === currentModelKey, disabled: option.disabled }"
@click.stop="handleHeaderModelSelect(option.key, option.disabled)"
:disabled="streamingMessage || !isConnected || option.disabled"
>
<div class="item-label">{{ option.label }}</div>
<div class="item-desc">{{ option.description }}</div>
<span v-if="option.key === currentModelKey" class="item-check">✓</span>
</button>
</div>
</div>
<div class="dropdown-column" data-tutorial="header-runmode-options">
<div class="dropdown-title">运行模式</div>
<button
v-for="option in headerRunModeOptions"
:key="option.value"
type="button"
class="dropdown-item"
:class="{ active: option.value === resolvedRunMode }"
@click.stop="handleHeaderRunModeSelect(option.value)"
:disabled="streamingMessage || !isConnected"
>
<div class="item-label">{{ option.label }}</div>
<div class="item-desc">{{ option.desc }}</div>
<span v-if="option.value === resolvedRunMode" class="item-check">✓</span>
</button>
</div>
</div>
</transition>
</div>
<ChatArea
v-show="chatDisplayMode === 'chat'"
ref="messagesArea"
:messages="messages"
:icon-style="iconStyle"
:expanded-blocks="expandedBlocks"
:render-markdown="renderMarkdown"
:toggle-block="toggleBlock"
:handle-thinking-scroll="handleThinkingScroll"
:streaming-message="streamingMessage"
:get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText"
:get-tool-animation-class="getToolAnimationClass"
:get-tool-description="getToolDescription"
:format-search-topic="formatSearchTopic"
:format-search-time="formatSearchTime"
:format-search-domains="formatSearchDomains"
@stick-state-change="handleStickStateChange"
@user-scroll-intent="handleUserScrollIntent"
/>
<VirtualMonitorSurface v-if="chatDisplayMode === 'monitor'" />
<div v-if="blankHeroActive" class="blank-hero-overlay">
<span class="icon icon-lg" :style="iconStyle('bot')" aria-hidden="true"></span>
<p class="blank-hero-text">{{ blankWelcomeText }}</p>
</div>
<transition name="scroll-to-bottom-fade">
<div v-if="showScrollToBottomButton" class="scroll-lock-toggle">
<button @click="toggleScrollLock" class="scroll-lock-btn">
<svg
viewBox="0 0 24 24"
aria-hidden="true"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 5v12" />
<path d="M7 13l5 5 5-5" />
</svg>
</button>
</div>
</transition>
<div class="composer-container" :class="{ 'blank-hero-mode': composerHeroActive }">
<InputComposer
ref="inputComposer"
:input-message="inputMessage"
:input-is-multiline="inputIsMultiline"
:input-is-focused="inputIsFocused"
:is-connected="isConnected"
:streaming-message="composerStreamingForInput"
:input-locked="displayLockEngaged"
:uploading="uploading"
:media-uploading="mediaUploading"
:thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:model-menu-open="modelMenuOpen"
:model-options="modelOptions"
:current-model-key="currentModelKey"
:quick-menu-open="quickMenuOpen"
:tool-menu-open="toolMenuOpen"
:mode-menu-open="modeMenuOpen"
:tool-settings="toolSettings"
:tool-settings-loading="toolSettingsLoading"
:settings-open="settingsOpen"
:compressing="compressing || compressionInProgress"
:current-conversation-id="currentConversationId"
:icon-style="iconStyle"
:tool-category-icon="toolCategoryIcon"
:selected-images="selectedImages"
:selected-videos="selectedVideos"
:block-upload="policyUiBlocks.block_upload"
:block-tool-toggle="policyUiBlocks.block_tool_toggle"
:block-realtime-terminal="policyUiBlocks.block_realtime_terminal"
:block-focus-panel="policyUiBlocks.block_focus_panel"
:block-token-panel="policyUiBlocks.block_token_panel"
:block-compress-conversation="policyUiBlocks.block_compress_conversation"
:block-conversation-review="policyUiBlocks.block_conversation_review"
:current-permission-mode="currentPermissionMode"
:permission-menu-open="permissionMenuOpen"
:permission-options="permissionModeOptions"
:execution-mode-enabled="executionModeEnabled"
:current-execution-mode="currentExecutionMode"
:execution-mode-options="executionModeOptions"
:current-context-tokens="currentContextTokens"
:versioning-enabled="versioningEnabled"
:runtime-queued-messages="runtimeQueuedMessages"
:user-question-minimized="userQuestionMinimized"
:pending-user-question-count="pendingUserQuestions.length"
:goal-mode-armed="goalModeArmed"
:goal-running="goalRunning"
:goal-progress="goalProgress"
@restore-user-question="restoreUserQuestionDialog"
@update:input-message="inputSetMessage"
@input-change="handleInputChange"
@input-focus="handleInputFocus"
@input-blur="handleInputBlur"
@toggle-quick-menu="toggleQuickMenu"
@send-message="sendMessage"
@send-or-stop="handleSendOrStop"
@quick-upload="handleQuickUpload"
@toggle-tool-menu="toggleToolMenu"
@toggle-mode-menu="toggleModeMenu"
@toggle-model-menu="toggleModelMenu"
@select-run-mode="handleModeSelect"
@select-model="handleModelSelect"
@toggle-settings="toggleSettings"
@update-tool-category="updateToolCategory"
@realtime-terminal="handleRealtimeTerminalClick"
@toggle-focus-panel="handleFocusPanelToggleClick"
@toggle-token-panel="(val) => handleTokenPanelToggleClick(val)"
@compress-conversation="handleCompressConversationClick"
@toggle-approval-panel="handleApprovalPanelToggleClick"
@file-selected="handleFileSelected"
@pick-images="openImagePicker"
@pick-video="openVideoPicker"
@remove-image="handleRemoveImage"
@remove-video="handleRemoveVideo"
@open-review="openReviewDialog"
@toggle-permission-menu="togglePermissionMenu"
@change-permission-mode="changePermissionMode"
@change-execution-mode="changeExecutionMode"
@open-path-authorization="openPathAuthorizationDialog"
@open-versioning-dialog="openVersioningDialog"
@guide-runtime-message="handleGuideRuntimeMessage"
@delete-runtime-message="handleDeleteRuntimeMessage"
@composer-height-change="handleComposerHeightChange"
@toggle-goal-mode="handleToggleGoalMode"
@open-goal-dialog="goalDialogOpen = true"
/>
</div>
</main>
<div
v-if="!isMobileViewport && rightCollapsed"
class="resize-handle"
@mousedown="startResize('right', $event)"
></div>
</div>
<transition name="desktop-approval-fade">
<div v-if="!isMobileViewport && !rightCollapsed" class="desktop-approval-overlay">
<div
class="desktop-approval-sheet"
:style="{ width: Math.min(rightWidth || 420, 520) + 'px' }"
>
<ToolApprovalPanel
:collapsed="false"
:width="Math.min(rightWidth || 420, 520)"
:approvals="pendingToolApprovals"
:deciding-approval-ids="decidingApprovalIds"
:auto-approval-feed-lines="autoApprovalFeedLines"
:auto-approval-final-message="autoApprovalFinalMessage"
:auto-approval-title="autoApprovalTitle"
@switch-unrestricted="handleSwitchPermissionToUnrestricted"
@approve="approveToolApproval"
@reject="rejectToolApproval"
@close="rightCollapsed = true"
/>
</div>
</div>
</transition>
<PersonalizationDrawer />
<PathAuthorizationDialog
:open="pathAuthorizationDialogOpen"
:value="pathAuthorizationDraft"
:mode="pathAuthorizationMode"
:saving="pathAuthorizationSaving"
@update:value="(v) => (pathAuthorizationDraft = v)"
@update:mode="setPathAuthorizationMode"
@close="closePathAuthorizationDialog"
@save="savePathAuthorization"
/>
<transition name="overlay-fade">
<ImagePicker
v-if="imagePickerOpen"
:open="imagePickerOpen"
:entries="imageEntries"
:initial-selected="selectedImages"
:loading="imageLoading"
:uploading="mediaUploading"
@close="closeImagePicker"
@confirm="handleImagesConfirmed"
@local-files="handleLocalImageFiles"
/>
</transition>
<transition name="overlay-fade">
<VideoPicker
v-if="videoPickerOpen"
:open="videoPickerOpen"
:entries="videoEntries"
:initial-selected="selectedVideos"
:loading="videoLoading"
:uploading="mediaUploading"
@close="closeVideoPicker"
@confirm="handleVideosConfirmed"
@local-files="handleLocalVideoFiles"
/>
</transition>
<transition name="overlay-fade">
<ConversationReviewDialog
v-if="reviewDialogOpen"
:open="reviewDialogOpen"
:conversations="conversations"
:current-conversation-id="currentConversationId"
:selected-id="reviewSelectedConversationId"
:loading="conversationsLoading"
:loading-more="loadingMoreConversations"
:has-more="hasMoreConversations"
:submitting="reviewSubmitting"
:preview="reviewPreviewLines"
:preview-loading="reviewPreviewLoading"
:preview-error="reviewPreviewError"
:preview-limit="reviewPreviewLimit"
:send-to-model="reviewSendToModel"
:generated-path="reviewGeneratedPath"
@close="reviewDialogOpen = false"
@select="handleReviewSelect"
@load-more="loadMoreConversations"
@toggle-send="reviewSendToModel = $event"
@confirm="handleConfirmReview"
/>
</transition>
<SubAgentActivityDialog />
<BackgroundCommandDialog />
<GoalProgressDialog
:open="goalDialogOpen"
:progress="goalProgress"
@close="goalDialogOpen = false"
/>
<UserQuestionDialog
:visible="userQuestionDialogVisible && !userQuestionMinimized && pendingUserQuestions.length > 0"
:questions="pendingUserQuestions"
:active-index="userQuestionActiveIndex"
:submitting-ids="answeringUserQuestionIds"
@minimize="minimizeUserQuestionDialog"
@update:active-index="userQuestionActiveIndex = $event"
@submit="submitUserQuestionAnswers"
/>
<transition name="overlay-fade">
<VersioningDialog
v-if="versioningDialogOpen"
:host-mode="versioningHostMode"
:enabled="versioningEnabled"
:tracking-mode="versioningTrackingMode"
:loading="versioningLoading"
:items="versioningCheckpoints"
:selected-seq="versioningSelectedSeq"
:detail="versioningSelectedDetail"
:detail-loading="versioningDetailLoading"
:restoring="versioningRestoring"
:restore-mode="versioningRestoreMode"
:workspace-matched="versioningWorkspaceMatched"
@close="versioningDialogOpen = false"
@refresh="refreshVersioningDialog"
@toggle-enabled="toggleConversationVersioning"
@select="selectVersioningCheckpoint"
@update:restore-mode="versioningRestoreMode = $event"
@update:tracking-mode="handleVersioningTrackingModeChange"
@confirm="confirmVersioningRestore"
/>
</transition>
<transition name="overlay-fade">
<HostWorkspaceCreateDialog
v-if="hostWorkspaceCreateDialogOpen"
:open="hostWorkspaceCreateDialogOpen"
:path-value="hostWorkspaceCreatePath"
:label-value="hostWorkspaceCreateLabel"
:submitting="hostWorkspaceCreateSubmitting"
:error-message="hostWorkspaceCreateError"
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
@close="closeHostWorkspaceCreateDialog"
@submit="submitHostWorkspaceCreate"
@update:path-value="hostWorkspaceCreatePath = $event"
@update:label-value="hostWorkspaceCreateLabel = $event"
/>
</transition>
<transition name="overlay-fade">
<HostWorkspaceManageDialog
v-if="hostWorkspaceManageDialogOpen"
:workspaces="hostWorkspaces"
:current-workspace-id="currentHostWorkspaceId"
:default-workspace-id="defaultHostWorkspaceId"
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
:busy="hostWorkspaceCreateSubmitting || hostWorkspaceManageSubmitting"
:create-submitting="hostWorkspaceCreateSubmitting"
:error-message="hostWorkspaceCreateError"
@close="closeHostWorkspaceManageDialog"
@create="submitHostWorkspaceCreateFromManage"
@rename="renameHostWorkspace"
@delete="deleteHostWorkspace"
/>
</transition>
<TutorialOverlay v-if="tutorialStore.running" />
<NewUserTutorialPrompt
:visible="tutorialPromptVisible"
:username="tutorialPromptUsername"
:loading="tutorialPromptLoading"
@start="handleNewUserTutorialStart"
@skip="handleNewUserTutorialSkip"
/>
<div
v-if="isMobileViewport"
class="mobile-panel-trigger"
:class="{ 'is-hidden': Boolean(activeMobileOverlay) }"
ref="mobilePanelTrigger"
>
<div class="mobile-panel-topbar">
<button
type="button"
class="mobile-panel-fab"
data-tutorial="mobile-menu-trigger"
aria-label="切换工作区"
@click="toggleMobileOverlayMenu"
>
<img :src="mobilePanelIcon" alt="" aria-hidden="true" />
</button>
<button
type="button"
class="mobile-topbar-selector"
data-tutorial="header-model-selector-mobile"
:class="{ open: headerMenuOpen }"
@click.stop="toggleHeaderMenu"
:disabled="!isConnected"
>
<span class="mobile-selector-label">
<span class="mobile-selector-model">{{ currentModelLabel }}</span>
<span class="mobile-selector-sep">·</span>
<span class="mobile-selector-mode">{{ headerRunModeLabel }}</span>
</span>
</button>
<div class="mobile-topbar-title" :title="currentConversationTitle || '未命名对话'">
{{ currentConversationTitle || '未命名对话' }}
</div>
</div>
<transition name="mobile-panel-menu">
<div v-if="mobileOverlayMenuOpen" class="mobile-panel-menu mobile-panel-menu--dropdown">
<button
type="button"
class="dropdown-item mobile-menu-item"
data-tutorial="mobile-menu-conversation"
aria-label="对话记录"
@click="openMobileOverlay('conversation')"
>
<svg class="mobile-menu-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 5.5c0-1.38 1.12-2.5 2.5-2.5h11c1.38 0 2.5 1.12 2.5 2.5v7.5c0 1.38-1.12 2.5-2.5 2.5h-4.8l-2.9 2.7.5-2.7H6.5c-1.38 0-2.5-1.12-2.5-2.5V5.5z"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8 8.5h8"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
<path
d="M8 11.5h5"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
</svg>
<div class="item-label">对话记录</div>
</button>
<button
type="button"
class="dropdown-item mobile-menu-item"
data-tutorial="mobile-menu-workspace"
aria-label="工作文件"
@click="openMobileOverlay('workspace')"
>
<img
class="mobile-menu-icon"
:src="mobileMenuIcons.workspace"
alt=""
aria-hidden="true"
/>
<div class="item-label">工作文件</div>
</button>
<button
type="button"
class="dropdown-item mobile-menu-item mobile-menu-item--personal"
data-tutorial="mobile-menu-personal"
aria-label="个人空间"
@click="handleMobilePersonalClick"
>
<img
class="mobile-menu-icon"
:src="mobileMenuIcons.personal"
alt=""
aria-hidden="true"
/>
<div class="item-label">个人空间</div>
</button>
<button
type="button"
class="dropdown-item mobile-menu-item"
data-tutorial="mobile-menu-new-chat"
aria-label="新建对话"
@click="createNewConversation"
>
<img
class="mobile-menu-icon"
:src="mobileMenuIcons.newChat"
alt=""
aria-hidden="true"
/>
<div class="item-label">新建对话</div>
</button>
<button
v-if="isAppShell"
type="button"
class="dropdown-item mobile-menu-item mobile-menu-item--refresh"
aria-label="刷新页面"
@click="handleMobileRefreshClick"
>
<img
class="mobile-menu-icon"
:src="mobileMenuIcons.refresh"
alt=""
aria-hidden="true"
/>
<div class="item-label">刷新页面</div>
</button>
</div>
</transition>
<transition name="header-menu">
<div
v-if="headerMenuOpen && isMobileViewport"
class="model-mode-dropdown model-mode-dropdown--mobile"
ref="headerMenu"
>
<div class="dropdown-column" data-tutorial="header-model-options">
<div class="dropdown-title">模型</div>
<div class="dropdown-list dropdown-list--models">
<button
v-for="option in modelOptions"
:key="option.key"
type="button"
class="dropdown-item"
:class="{ active: option.key === currentModelKey, disabled: option.disabled }"
@click.stop="handleHeaderModelSelect(option.key, option.disabled)"
:disabled="streamingMessage || !isConnected || option.disabled"
>
<div class="item-label">{{ option.label }}</div>
<span v-if="option.key === currentModelKey" class="item-check">✓</span>
</button>
</div>
</div>
<div class="dropdown-column" data-tutorial="header-runmode-options">
<div class="dropdown-title">运行模式</div>
<button
v-for="option in headerRunModeOptions"
:key="option.value"
type="button"
class="dropdown-item"
:class="{ active: option.value === resolvedRunMode }"
@click.stop="handleHeaderRunModeSelect(option.value)"
:disabled="streamingMessage || !isConnected"
>
<div class="item-label">{{ option.label }}</div>
<span v-if="option.value === resolvedRunMode" class="item-check">✓</span>
</button>
</div>
</div>
</transition>
</div>
<transition name="mobile-panel-overlay">
<div
v-if="isMobileViewport && activeMobileOverlay === 'conversation'"
class="mobile-panel-overlay mobile-panel-overlay--left"
@click.self="closeMobileOverlay"
>
<div class="mobile-panel-sheet mobile-panel-sheet--conversation">
<ConversationSidebar
class="mobile-overlay-content"
:icon-style="iconStyle"
:format-time="formatTime"
:running-tasks="runningWorkspaceTasks"
:current-workspace-id="currentHostWorkspaceId"
:display-mode="chatDisplayMode"
:display-mode-disabled="displayModeSwitchDisabled"
:show-collapse-button="true"
collapse-button-variant="close"
@toggle="closeMobileOverlay"
@create="createNewConversation"
@search="handleSidebarSearchInput"
@search-submit="handleSidebarSearchSubmit"
@search-more="loadMoreSearchResults"
@select="handleMobileOverlaySelect($event)"
@select-running-task="openRunningWorkspaceTask"
@load-more="loadMoreConversations"
@personal="openPersonalPage"
@delete="deleteConversation"
@duplicate="duplicateConversation"
@toggle-display-mode="handleDisplayModeToggle"
/>
</div>
</div>
</transition>
<transition name="mobile-panel-overlay">
<div
v-if="isMobileViewport && activeMobileOverlay === 'workspace'"
class="mobile-panel-overlay mobile-panel-overlay--left"
@click.self="closeMobileOverlay"
>
<div class="mobile-panel-sheet mobile-panel-sheet--workspace">
<button
type="button"
class="mobile-overlay-close"
data-tutorial="mobile-workspace-close"
aria-label="关闭面板"
@click="closeMobileOverlay"
>
×
</button>
<LeftPanel
class="mobile-overlay-content"
:width="Math.min(leftWidth, 420)"
:icon-style="iconStyle"
:agent-version="agentVersion"
:thinking-mode="thinkingMode"
:run-mode="resolvedRunMode"
:is-connected="isConnected"
:panel-menu-open="panelMenuOpen"
:panel-mode="panelMode"
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
:host-workspaces="hostWorkspaces"
:host-workspace-id="currentHostWorkspaceId"
:host-workspace-default-id="defaultHostWorkspaceId"
:host-workspace-switching="hostWorkspaceSwitching"
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
@switch-host-workspace="handleHostWorkspaceSwitch"
@create-host-workspace="handleCreateHostWorkspace"
@manage-host-workspace="openHostWorkspaceManageDialog"
/>
</div>
</div>
</transition>
<transition name="mobile-panel-overlay">
<div
v-if="isMobileViewport && activeMobileOverlay === 'approval'"
class="mobile-panel-overlay mobile-panel-overlay--right mobile-approval-overlay"
@click.self="closeMobileOverlay('backdrop-click')"
>
<div class="mobile-panel-sheet mobile-panel-sheet--approval">
<ToolApprovalPanel
class="mobile-overlay-content"
:collapsed="false"
:width="Math.min(rightWidth || 420, 460)"
:approvals="pendingToolApprovals"
:deciding-approval-ids="decidingApprovalIds"
:auto-approval-feed-lines="autoApprovalFeedLines"
:auto-approval-final-message="autoApprovalFinalMessage"
@switch-unrestricted="handleSwitchPermissionToUnrestricted"
@approve="approveToolApproval"
@reject="rejectToolApproval"
@close="closeMobileOverlay('approval-panel-close-btn')"
/>
</div>
</div>
</transition>
</template>
</AppShell>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import appOptions from './app';
import VideoPicker from './components/overlay/VideoPicker.vue';
import { useTutorialStore } from './stores/tutorial';
const VirtualMonitorSurface = defineAsyncComponent(
() => import('./components/chat/VirtualMonitorSurface.vue')
);
const PathAuthorizationDialog = defineAsyncComponent(
() => import('./components/overlay/PathAuthorizationDialog.vue')
);
const tutorialStore = useTutorialStore();
const mobilePanelIcon = new URL('../icons/align-left.svg', import.meta.url).href;
const isAppShell =
typeof window !== 'undefined' &&
(new URLSearchParams(window.location.search).has('app_shell') ||
Boolean((window as any)?.AndroidThemeBridge));
const mobileMenuIcons = {
workspace: new URL('../icons/folder.svg', import.meta.url).href,
personal: new URL('../icons/user.svg', import.meta.url).href,
newChat: new URL('../icons/pencil.svg', import.meta.url).href,
refresh: new URL('../icons/refresh-cw.svg', import.meta.url).href
};
defineOptions(appOptions);
</script>