feat(input): slash 菜单新增 // 直达 skill 与五个功能项,移除压缩二次确认

- // 键入直达 skills 子菜单(Escape 退回 root 不自动重进)
- 新增:新建对话(/new /clear 均可匹配)、网络权限、工作模式、
  切换工作区(host)/切换项目(docker)、对话类型(仅新对话可切)
- 压缩对话点击即执行,去掉 confirmAction 二次确认
This commit is contained in:
JOJO 2026-08-13 17:38:09 +08:00
parent 8d10effd56
commit 4f5b27f57c
3 changed files with 182 additions and 16 deletions

View File

@ -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"
/>

View File

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

View File

@ -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=键入 // 直达(跟随斜杠数退回 rootmenu=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,