agent-Specialization/static/src/components/input/QuickMenu.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

254 lines
8.1 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>
<transition name="quick-menu">
<div v-if="open" class="quick-menu" @click.stop>
<button
type="button"
class="menu-entry"
data-tutorial="quick-upload"
@click="$emit('quick-upload')"
:disabled="!isConnected || uploading"
>
{{ uploading ? '上传中...' : '上传文件' }}
</button>
<button
type="button"
class="menu-entry"
data-tutorial="quick-review"
@click.stop="$emit('open-review')"
:disabled="!isConnected || streamingMessage"
>
对话回顾
</button>
<button
v-if="currentModelSupportsImage"
type="button"
class="menu-entry"
data-tutorial="quick-send-image"
@click.stop="$emit('pick-images')"
:disabled="!isConnected || streamingMessage"
>
发送图片
</button>
<button
v-if="currentModelSupportsVideo"
type="button"
class="menu-entry"
data-tutorial="quick-send-video"
@click.stop="$emit('pick-video')"
:disabled="!isConnected || streamingMessage"
>
发送视频
</button>
<button
type="button"
class="menu-entry has-submenu"
data-tutorial="quick-tool-menu"
@click.stop="$emit('toggle-tool-menu')"
:disabled="!isConnected"
>
工具禁用
<span class="entry-arrow"></span>
</button>
<button
type="button"
class="menu-entry"
:class="{ 'goal-entry-active': goalModeArmed || goalRunning || goalCompleted }"
@click.stop="$emit('toggle-goal-mode')"
:disabled="!isConnected"
>
目标模式
<span class="entry-arrow">
<template v-if="goalRunning">运行中</template>
<template v-else-if="goalCompleted">完成</template>
<template v-else>{{ goalModeArmed ? '已就绪' : '' }}</template>
</span>
</button>
<button
type="button"
class="menu-entry has-submenu"
data-tutorial="quick-settings-menu"
@click.stop="$emit('toggle-settings')"
:disabled="!isConnected"
>
设置
<span class="entry-arrow"></span>
</button>
<transition name="submenu-slide">
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
<div class="submenu-status" v-if="toolSettingsLoading">正在同步工具状态...</div>
<div v-else-if="!toolSettings.length" class="submenu-empty">暂无可控工具</div>
<div v-else class="submenu-list tool-submenu-list">
<button
v-for="category in toolSettings"
:key="category.id"
type="button"
class="menu-entry submenu-entry"
:class="{ disabled: !category.enabled || category.locked }"
@click.stop="$emit('update-tool-category', category.id, !category.enabled)"
:disabled="streamingMessage || !isConnected || toolSettingsLoading"
>
<span class="submenu-label icon-label">
<span
class="icon icon-sm"
:style="getIconStyle(toolCategoryIcon(category.id))"
aria-hidden="true"
></span>
<span>{{ category.label }}</span>
</span>
<span class="entry-arrow">
<template v-if="category.locked"> 被管理员锁定 </template>
<template v-else>
{{ category.enabled ? '禁用' : '启用' }}
</template>
</span>
</button>
</div>
</div>
</transition>
<transition name="submenu-slide">
<div class="quick-submenu settings-submenu" v-if="settingsOpen">
<div class="submenu-list">
<button
type="button"
class="menu-entry submenu-entry"
@click="$emit('realtime-terminal')"
:disabled="!isConnected"
>
实时终端
</button>
<button
type="button"
class="menu-entry submenu-entry"
data-tutorial="settings-token-panel"
@click="console.log('[UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true'); $emit('toggle-token-panel', true)"
:disabled="!currentConversationId"
>
用量统计
</button>
<button
type="button"
class="menu-entry submenu-entry"
@click="$emit('compress-conversation')"
:disabled="compressing || streamingMessage || !isConnected"
>
{{ compressing ? '压缩中...' : '压缩对话' }}
</button>
<button
type="button"
class="menu-entry submenu-entry"
@click="$emit('toggle-approval-panel')"
:disabled="!currentConversationId"
>
审批面板
</button>
<button
v-if="executionModeEnabled"
type="button"
class="menu-entry submenu-entry"
@click="$emit('open-path-authorization')"
>
路径授权
</button>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script setup lang="ts">
import { computed } from 'vue';
defineOptions({ name: 'QuickMenu' });
const props = defineProps<{
open: boolean;
isConnected: boolean;
uploading: boolean;
streamingMessage: boolean;
thinkingMode: boolean;
toolMenuOpen: boolean;
toolSettings: Array<{ id: string; label: string; enabled: boolean }>;
toolSettingsLoading: boolean;
settingsOpen: boolean;
compressing: boolean;
currentConversationId: string | null;
iconStyle?: (key: string) => Record<string, string>;
toolCategoryIcon: (categoryId: string) => string;
modeMenuOpen: boolean;
runMode?: 'fast' | 'thinking' | 'deep';
modelMenuOpen: boolean;
modelOptions: Array<{
key: string;
label: string;
description: string;
disabled?: boolean;
supportsImage?: boolean;
supportsVideo?: boolean;
}>;
currentModelKey: string;
blockUpload?: boolean;
blockToolToggle?: boolean;
blockRealtimeTerminal?: boolean;
blockFocusPanel?: boolean;
blockTokenPanel?: boolean;
blockCompressConversation?: boolean;
blockConversationReview?: boolean;
executionModeEnabled?: boolean;
goalModeArmed?: boolean;
goalRunning?: boolean;
goalProgress?: Record<string, any> | null;
}>();
defineEmits<{
(event: 'quick-upload'): void;
(event: 'toggle-tool-menu'): void;
(event: 'toggle-settings'): void;
(event: 'update-tool-category', id: string, enabled: boolean): void;
(event: 'realtime-terminal'): void;
(event: 'toggle-token-panel', fromSettingsMenu?: boolean): void;
(event: 'compress-conversation'): void;
(event: 'toggle-approval-panel'): void;
(event: 'toggle-mode-menu'): void;
(event: 'select-run-mode', mode: 'fast' | 'thinking' | 'deep'): void;
(event: 'toggle-model-menu'): void;
(event: 'select-model', key: string): void;
(event: 'open-review'): void;
(event: 'pick-video'): void;
(event: 'open-path-authorization'): void;
(event: 'toggle-goal-mode'): void;
}>();
const getIconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
const currentModelSupportsImage = computed(() => {
const found = props.modelOptions?.find((m) => m.key === props.currentModelKey) as any;
return !!found?.supportsImage;
});
const currentModelSupportsVideo = computed(() => {
const found = props.modelOptions?.find((m) => m.key === props.currentModelKey) as any;
return !!found?.supportsVideo;
});
const goalCompleted = computed(() => String(props.goalProgress?.status || '').toLowerCase() === 'done');
</script>
<style scoped>
.submenu-desc {
display: block;
font-size: 12px;
color: var(--text-secondary, #7f8792);
margin-top: 2px;
}
.menu-entry.goal-entry-active {
color: var(--claude-accent, #da7756);
}
.menu-entry.goal-entry-active .entry-arrow {
color: var(--claude-accent, #da7756);
font-size: 12px;
}
</style>