feat(input): slash 菜单新增 // 直达 skill 与五个功能项,移除压缩二次确认
- // 键入直达 skills 子菜单(Escape 退回 root 不自动重进) - 新增:新建对话(/new /clear 均可匹配)、网络权限、工作模式、 切换工作区(host)/切换项目(docker)、对话类型(仅新对话可切) - 压缩对话点击即执行,去掉 confirmAction 二次确认
This commit is contained in:
parent
8d10effd56
commit
4f5b27f57c
@ -310,6 +310,10 @@
|
||||
:current-work-mode="currentWorkMode"
|
||||
:work-mode-menu-open="workModeMenuOpen"
|
||||
:work-mode-options="workModeOptions"
|
||||
:host-workspaces="hostWorkspaces"
|
||||
:current-host-workspace-id="currentHostWorkspaceId"
|
||||
:host-workspace-switching="hostWorkspaceSwitching"
|
||||
:workspace-kind="versioningHostMode ? 'workspace' : (dockerProjectMode ? 'project' : null)"
|
||||
:main-chat-idle="mainChatIdle"
|
||||
:active-sub-agent-count="activeSubAgentCount"
|
||||
:avatar-status="showStatusAvatar && messages.length > 0 ? avatarStatus : null"
|
||||
@ -357,6 +361,9 @@
|
||||
@select-new-conversation-type="handleSelectNewConversationType"
|
||||
@toggle-work-mode-menu="toggleWorkModeMenu"
|
||||
@change-work-mode="changeWorkMode"
|
||||
@new-conversation="createNewConversation"
|
||||
@switch-host-workspace="handleHostWorkspaceSwitch"
|
||||
@fetch-host-workspaces="fetchHostWorkspaces"
|
||||
@open-goal-dialog="goalDialogOpen = true"
|
||||
@open-git-changes-panel="openGitChangesPanel"
|
||||
/>
|
||||
|
||||
@ -32,16 +32,6 @@ export const chatMethods = {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.confirmAction({
|
||||
title: '压缩对话',
|
||||
message: '确定要压缩当前对话记录吗?较早的消息会被折叠并生成压缩摘要,当前对话 ID 保持不变。',
|
||||
confirmText: '压缩',
|
||||
cancelText: '取消'
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.compressing = true;
|
||||
this.compressionInProgress = true;
|
||||
this.compressionConversationId = this.currentConversationId;
|
||||
|
||||
@ -701,6 +701,9 @@ const emit = defineEmits([
|
||||
'select-new-conversation-type',
|
||||
'toggle-work-mode-menu',
|
||||
'change-work-mode',
|
||||
'new-conversation',
|
||||
'switch-host-workspace',
|
||||
'fetch-host-workspaces',
|
||||
'stop-all-sub-agents'
|
||||
]);
|
||||
|
||||
@ -783,6 +786,20 @@ const props = defineProps<{
|
||||
workModeMenuOpen?: boolean;
|
||||
/** 运行模式选项 */
|
||||
workModeOptions?: Array<{ value: string; label: string; description: string }>;
|
||||
/** host/docker 项目模式下的工作区列表(/菜单「切换工作区」数据源) */
|
||||
hostWorkspaces?: Array<{
|
||||
workspace_id: string;
|
||||
label: string;
|
||||
path?: string;
|
||||
is_current?: boolean;
|
||||
running_task_count?: number;
|
||||
}>;
|
||||
/** 当前工作区 id */
|
||||
currentHostWorkspaceId?: string;
|
||||
/** 工作区切换进行中 */
|
||||
hostWorkspaceSwitching?: boolean;
|
||||
/** 工作区文案类型:workspace=host 工作区;project=docker 项目;null=不显示工作区切换项 */
|
||||
workspaceKind?: 'workspace' | 'project' | null;
|
||||
/**
|
||||
* 主对话是否空闲(composerBusy 可能是 true,但只是因为后台子智能体在跑)。
|
||||
* 多智能体模式下,若主对话空闲,输入栏应处于“正常发送”状态,不是“停止”状态。
|
||||
@ -818,7 +835,11 @@ type SlashMenuMode =
|
||||
| 'permission'
|
||||
| 'execution'
|
||||
| 'model'
|
||||
| 'run-mode';
|
||||
| 'run-mode'
|
||||
| 'network'
|
||||
| 'work-mode'
|
||||
| 'workspace'
|
||||
| 'conversation-type';
|
||||
type SlashMenuItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
@ -842,6 +863,10 @@ const slashMenuTransitioning = ref(false);
|
||||
const slashHighlightTop = ref(4);
|
||||
/** 手动滚动后切到鼠标 hover 模式,隐藏 JS 高亮条 */
|
||||
const slashManualScrolled = ref(false);
|
||||
/** skills 子菜单进入来源:typed=键入 // 直达(跟随斜杠数退回 root);menu=root 菜单点入(不跟随) */
|
||||
const slashSkillsEntrySource = ref<'typed' | 'menu' | null>(null);
|
||||
/** Escape 从 // 直达的 skills 退回 root 后,抑制本次 token 存续期间的自动重进 */
|
||||
const slashDoubleSlashSuppressed = ref(false);
|
||||
let slashAnimId: number | null = null;
|
||||
/** 动画真正进行中(比 animId 更可靠,可覆盖 rAF 结束后的延迟 scroll 事件) */
|
||||
let slashAnimating = false;
|
||||
@ -1096,15 +1121,17 @@ const findSlashToken = () => {
|
||||
const { $from } = state.selection;
|
||||
const lineStart = $from.start();
|
||||
const beforeInLine = $from.parent.textBetween(0, $from.parentOffset, '\n', '\n');
|
||||
const match = /(^|[ \n])\/([^\s/]*)$/.exec(beforeInLine);
|
||||
const match = /(^|[ \n])(\/{1,2})([^\s/]*)$/.exec(beforeInLine);
|
||||
if (!match) return null;
|
||||
const query = match[2] || '';
|
||||
const start = $from.pos - query.length - 1;
|
||||
const slashes = match[2] || '/';
|
||||
const query = match[3] || '';
|
||||
const start = $from.pos - query.length - slashes.length;
|
||||
const end = $from.pos;
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
query
|
||||
query,
|
||||
doubleSlash: slashes.length === 2
|
||||
};
|
||||
};
|
||||
|
||||
@ -1142,6 +1169,8 @@ const closeSlashMenu = () => {
|
||||
slashMenuParentIndex.value = 0;
|
||||
slashHighlightTop.value = 4;
|
||||
slashManualScrolled.value = false;
|
||||
slashSkillsEntrySource.value = null;
|
||||
slashDoubleSlashSuppressed.value = false;
|
||||
};
|
||||
|
||||
const closeFileAtMenu = () => {
|
||||
@ -1435,6 +1464,13 @@ const executeSlashAction = (action: () => void) => {
|
||||
|
||||
const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const items: SlashMenuItem[] = [
|
||||
{
|
||||
id: 'new',
|
||||
label: '新建对话',
|
||||
description: '开始一个新的对话(new / clear)',
|
||||
disabled: !props.isConnected,
|
||||
action: () => emit('new-conversation')
|
||||
},
|
||||
{
|
||||
id: 'upload',
|
||||
label: '上传文件',
|
||||
@ -1470,9 +1506,23 @@ const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
{
|
||||
id: 'skills',
|
||||
label: '选择 AgentSkill',
|
||||
description: '插入一个 AgentSkill 引用',
|
||||
description: '插入一个 AgentSkill 引用(// 快捷直达)',
|
||||
mode: 'skills'
|
||||
},
|
||||
{
|
||||
id: 'conversation-type',
|
||||
label: '对话类型',
|
||||
description: 'agent / multi-agent · 智能体 / 多智能体(仅新对话可切换)',
|
||||
disabled: !props.isConnected,
|
||||
mode: 'conversation-type'
|
||||
},
|
||||
{
|
||||
id: 'work-mode',
|
||||
label: '切换工作模式',
|
||||
description: 'plan / ask / execute · 计划 / 询问 / 执行',
|
||||
disabled: !props.isConnected || props.streamingMessage,
|
||||
mode: 'work-mode'
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
label: '切换主题',
|
||||
@ -1540,6 +1590,15 @@ const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
disabled: !props.isConnected || props.streamingMessage,
|
||||
mode: 'permission'
|
||||
},
|
||||
{
|
||||
id: 'network',
|
||||
label: '网络权限',
|
||||
description: props.networkPermissionEnabled
|
||||
? 'restricted / full · 受限 / 完整'
|
||||
: '当前环境不支持网络权限切换',
|
||||
disabled: !props.isConnected || props.streamingMessage || !props.networkPermissionEnabled,
|
||||
mode: 'network'
|
||||
},
|
||||
...(props.executionModeEnabled
|
||||
? [
|
||||
{
|
||||
@ -1551,6 +1610,17 @@ const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
} as SlashMenuItem
|
||||
]
|
||||
: []),
|
||||
...(props.workspaceKind
|
||||
? [
|
||||
{
|
||||
id: 'workspace',
|
||||
label: props.workspaceKind === 'project' ? '切换项目' : '切换工作区',
|
||||
description: 'workspace / project · 工作区 / 项目',
|
||||
disabled: !props.isConnected || props.streamingMessage,
|
||||
mode: 'workspace'
|
||||
} as SlashMenuItem
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'versioning',
|
||||
label: '版本控制',
|
||||
@ -1686,6 +1756,56 @@ const runModeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const networkSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const options = props.networkPermissionOptions || [];
|
||||
const current = String(props.currentNetworkPermission || '');
|
||||
return options.map((opt) => ({
|
||||
id: `network:${opt.value}`,
|
||||
label: opt.label,
|
||||
description: `${opt.value === current ? '当前 · ' : ''}${opt.description || ''}`,
|
||||
action: () => emit('change-network-permission', opt.value)
|
||||
}));
|
||||
});
|
||||
|
||||
const workModeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const options = props.workModeOptions || [];
|
||||
const current = String(props.currentWorkMode || '');
|
||||
return options.map((opt) => ({
|
||||
id: `workmode:${opt.value}`,
|
||||
label: opt.label,
|
||||
description: `${opt.value === current ? '当前 · ' : ''}${opt.description || ''}`,
|
||||
action: () => emit('change-work-mode', opt.value)
|
||||
}));
|
||||
});
|
||||
|
||||
const workspaceSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const list = Array.isArray(props.hostWorkspaces) ? props.hostWorkspaces : [];
|
||||
const current = String(props.currentHostWorkspaceId || '');
|
||||
const switching = !!props.hostWorkspaceSwitching;
|
||||
return list.map((ws) => ({
|
||||
id: `workspace:${ws.workspace_id}`,
|
||||
label: ws.label,
|
||||
description: `${ws.workspace_id === current ? '当前 · ' : ''}${ws.path || ''}`,
|
||||
disabled: switching || ws.workspace_id === current,
|
||||
action: () => emit('switch-host-workspace', ws.workspace_id)
|
||||
}));
|
||||
});
|
||||
|
||||
const conversationTypeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
// 已打开对话时类型不可变(类型是创建时确定的对话属性),子菜单项整体锁定
|
||||
const locked = agentTypeLocked.value;
|
||||
const current = String(props.newConversationType || 'agent');
|
||||
return agentTypeOptions.map((opt) => ({
|
||||
id: `convtype:${opt.value}`,
|
||||
label: opt.label,
|
||||
description: locked
|
||||
? '对话类型创建后不可变,仅新对话可切换'
|
||||
: `${opt.value === current ? '当前 · ' : ''}${opt.description || ''}`,
|
||||
disabled: locked,
|
||||
action: () => emit('select-new-conversation-type', opt.value as 'agent' | 'multi_agent')
|
||||
}));
|
||||
});
|
||||
|
||||
const skillSlashMenuItems = computed<SlashMenuItem[]>(() =>
|
||||
filteredSkills.value.map((skill) => ({
|
||||
id: `skill:${skill.path}`,
|
||||
@ -1714,6 +1834,18 @@ const activeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
if (slashMenuMode.value === 'run-mode') {
|
||||
return runModeSlashMenuItems.value;
|
||||
}
|
||||
if (slashMenuMode.value === 'network') {
|
||||
return networkSlashMenuItems.value;
|
||||
}
|
||||
if (slashMenuMode.value === 'work-mode') {
|
||||
return workModeSlashMenuItems.value;
|
||||
}
|
||||
if (slashMenuMode.value === 'workspace') {
|
||||
return workspaceSlashMenuItems.value;
|
||||
}
|
||||
if (slashMenuMode.value === 'conversation-type') {
|
||||
return conversationTypeSlashMenuItems.value;
|
||||
}
|
||||
return rootSlashMenuItems.value;
|
||||
});
|
||||
|
||||
@ -1724,6 +1856,11 @@ const slashMenuAriaLabel = computed(() => {
|
||||
if (slashMenuMode.value === 'execution') return '执行环境选项';
|
||||
if (slashMenuMode.value === 'model') return '模型选项';
|
||||
if (slashMenuMode.value === 'run-mode') return '思考模式选项';
|
||||
if (slashMenuMode.value === 'network') return '网络权限选项';
|
||||
if (slashMenuMode.value === 'work-mode') return '工作模式选项';
|
||||
if (slashMenuMode.value === 'workspace')
|
||||
return props.workspaceKind === 'project' ? '项目选项' : '工作区选项';
|
||||
if (slashMenuMode.value === 'conversation-type') return '对话类型选项';
|
||||
return '快捷指令';
|
||||
});
|
||||
|
||||
@ -1733,6 +1870,11 @@ const slashMenuEmptyText = computed(() => {
|
||||
if (slashMenuMode.value === 'permission') return '无可用权限模式';
|
||||
if (slashMenuMode.value === 'execution') return '无可用执行环境';
|
||||
if (slashMenuMode.value === 'model') return '无可用模型';
|
||||
if (slashMenuMode.value === 'network') return '无可用网络权限选项';
|
||||
if (slashMenuMode.value === 'work-mode') return '无可用工作模式';
|
||||
if (slashMenuMode.value === 'workspace')
|
||||
return props.workspaceKind === 'project' ? '暂无其他项目' : '暂无其他工作区';
|
||||
if (slashMenuMode.value === 'conversation-type') return '无可用对话类型';
|
||||
return '没有匹配的指令';
|
||||
});
|
||||
|
||||
@ -1782,6 +1924,24 @@ const refreshSkillSlashState = () => {
|
||||
skillSlashActiveIndex.value = 0;
|
||||
}
|
||||
skillSlashQuery.value = token.query;
|
||||
// 「//」直达 skills 子菜单;仅键入来源跟随斜杠数量变化(菜单点入不受影响)
|
||||
if (
|
||||
token.doubleSlash &&
|
||||
slashMenuMode.value === 'root' &&
|
||||
!slashDoubleSlashSuppressed.value
|
||||
) {
|
||||
slashMenuMode.value = 'skills';
|
||||
slashSkillsEntrySource.value = 'typed';
|
||||
skillSlashActiveIndex.value = 0;
|
||||
} else if (
|
||||
!token.doubleSlash &&
|
||||
slashMenuMode.value === 'skills' &&
|
||||
slashSkillsEntrySource.value === 'typed'
|
||||
) {
|
||||
slashMenuMode.value = 'root';
|
||||
slashSkillsEntrySource.value = null;
|
||||
skillSlashActiveIndex.value = 0;
|
||||
}
|
||||
if (slashMenuMode.value === 'skills') {
|
||||
void loadSkills();
|
||||
}
|
||||
@ -1837,9 +1997,13 @@ const selectSlashMenuItem = (index = skillSlashActiveIndex.value) => {
|
||||
slashMenuParentIndex.value = index;
|
||||
slashMenuMode.value = item.mode;
|
||||
skillSlashActiveIndex.value = 0;
|
||||
slashSkillsEntrySource.value = item.mode === 'skills' ? 'menu' : null;
|
||||
if (item.mode === 'skills') {
|
||||
void loadSkills();
|
||||
}
|
||||
if (item.mode === 'workspace') {
|
||||
emit('fetch-host-workspaces');
|
||||
}
|
||||
scrollSkillSlashSelectionIntoMiddle();
|
||||
return;
|
||||
}
|
||||
@ -1889,6 +2053,11 @@ const handleSlashMenuKeydown = (event: KeyboardEvent): boolean => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
if (slashMenuMode.value !== 'root') {
|
||||
// 从 // 直达的 skills 退回 root 时,抑制本次 token 的自动重进(否则下一帧又跳回 skills)
|
||||
if (slashMenuMode.value === 'skills' && slashSkillsEntrySource.value === 'typed') {
|
||||
slashDoubleSlashSuppressed.value = true;
|
||||
}
|
||||
slashSkillsEntrySource.value = null;
|
||||
slashMenuMode.value = 'root';
|
||||
skillSlashActiveIndex.value = Math.min(
|
||||
slashMenuParentIndex.value,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user