feat(slash-menu): 扩展 / 菜单至19项,可见行数5→7,等比缩放样式

This commit is contained in:
JOJO 2026-06-11 02:56:04 +08:00
parent af4ec5c1a8
commit 87a2b2d15f
2 changed files with 164 additions and 21 deletions

View File

@ -535,7 +535,7 @@ const stadiumInput = ref<HTMLTextAreaElement | null>(null);
const fileUploadInput = ref<HTMLInputElement | null>(null);
const baselineComposerVisualHeight = ref<number>(0);
type SkillItem = { name: string; description: string; path: string };
type SlashMenuMode = 'root' | 'skills' | 'theme';
type SlashMenuMode = 'root' | 'skills' | 'theme' | 'permission' | 'execution' | 'model' | 'run-mode';
type SlashMenuItem = {
id: string;
label: string;
@ -556,7 +556,7 @@ const skillSlashList = ref<HTMLElement | null>(null);
const slashMenuMode = ref<SlashMenuMode>('root');
const slashMenuParentIndex = ref(0);
const slashMenuTransitioning = ref(false);
const slashHighlightTop = ref(5);
const slashHighlightTop = ref(4);
let slashAnimId: number | null = null;
const slashHighlightStyle = computed(() => {
@ -577,7 +577,7 @@ const animateSlash = (element: HTMLElement, targetScroll: number) => {
const startScroll = element.scrollTop;
const startHighlight = slashHighlightTop.value;
const distScroll = targetScroll - startScroll;
const targetHighlight = 5 + skillSlashActiveIndex.value * 43 - targetScroll;
const targetHighlight = 4 + skillSlashActiveIndex.value * 31 - targetScroll;
const distHighlight = targetHighlight - startHighlight;
const duration = 180;
const startTime = performance.now();
@ -723,7 +723,7 @@ const closeSlashMenu = () => {
skillSlashActiveIndex.value = 0;
slashMenuMode.value = 'root';
slashMenuParentIndex.value = 0;
slashHighlightTop.value = 5;
slashHighlightTop.value = 4;
};
const deleteSlashToken = () => {
@ -876,6 +876,81 @@ const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
action: () => {
void personalizationStore.openDrawer();
}
},
{
id: 'terminal',
label: '实时终端',
description: '打开实时终端面板',
disabled: !props.isConnected || props.blockRealtimeTerminal,
action: () => emit('realtime-terminal')
},
{
id: 'model',
label: '切换模型',
description: '选择对话使用的模型',
disabled: !props.isConnected || props.streamingMessage,
mode: 'model'
},
{
id: 'run-mode',
label: '思考模式',
description: '切换 fast / thinking / deep',
disabled: !props.isConnected || props.streamingMessage,
mode: 'run-mode'
},
{
id: 'permission',
label: '权限模式',
description: '切换 readonly / approval / auto / unrestricted',
disabled: !props.isConnected || props.streamingMessage,
mode: 'permission'
},
...(props.executionModeEnabled
? [{
id: 'execution',
label: '执行环境',
description: '切换 sandbox / direct',
disabled: !props.isConnected || props.streamingMessage,
mode: 'execution'
} as SlashMenuItem]
: []),
{
id: 'versioning',
label: '版本控制',
description: props.versioningEnabled ? '当前:开启' : '当前:关闭',
disabled: !props.isConnected || props.streamingMessage,
action: () => emit('open-versioning-dialog')
},
{
id: 'git-bar',
label: 'Git 状态栏',
description: (personalizationStore?.form?.show_git_status_bar !== false) ? '当前:显示' : '当前:隐藏',
action: () => {
const currentValue = personalizationStore?.form?.show_git_status_bar !== false;
const newValue = !currentValue;
if (personalizationStore?.form) {
personalizationStore.form.show_git_status_bar = newValue;
}
fetch('/api/personalization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ show_git_status_bar: newValue })
}).catch(() => {});
}
},
{
id: 'path-auth',
label: '路径授权',
description: '查看和管理路径授权',
disabled: !props.isConnected,
action: () => emit('open-path-authorization')
},
{
id: 'approval',
label: '审批面板',
description: '查看审批记录',
disabled: !props.isConnected,
action: () => emit('toggle-approval-panel')
}
];
const query = skillSlashQuery.value.trim().toLowerCase();
@ -917,6 +992,55 @@ const themeSlashMenuItems = computed<SlashMenuItem[]>(() => {
}));
});
const permissionSlashMenuItems = computed<SlashMenuItem[]>(() => {
const options = (props.permissionOptions || []);
const current = String(props.currentPermissionMode || '');
return options.map((opt) => ({
id: `perm:${opt.value}`,
label: opt.label,
description: `${opt.value === current ? '当前 · ' : ''}${opt.description || ''}`,
action: () => emit('change-permission-mode', opt.value)
}));
});
const executionSlashMenuItems = computed<SlashMenuItem[]>(() => {
const options = (props.executionModeOptions || []);
const current = String(props.currentExecutionMode || '');
return options.map((opt) => ({
id: `exec:${opt.value}`,
label: opt.label,
description: `${opt.value === current ? '当前 · ' : ''}${opt.description || ''}`,
action: () => emit('change-execution-mode', opt.value)
}));
});
const modelSlashMenuItems = computed<SlashMenuItem[]>(() => {
const list = Array.isArray(props.modelOptions) ? props.modelOptions : [];
const current = String(props.currentModelKey || '');
return list.map((model) => ({
id: `model:${model.key}`,
label: model.label,
description: `${model.key === current ? '当前 · ' : ''}${model.description || ''}`,
disabled: !!model.disabled,
action: () => emit('select-model', model.key)
}));
});
const runModeSlashMenuItems = computed<SlashMenuItem[]>(() => {
const modes: Array<{ value: string; label: string }> = [
{ value: 'fast', label: '快速' },
{ value: 'thinking', label: '思考' },
{ value: 'deep', label: '深度' }
];
const current = String(props.runMode || '');
return modes.map((mode) => ({
id: `runmode:${mode.value}`,
label: mode.label,
description: mode.value === current ? '当前' : '',
action: () => emit('select-run-mode', mode.value)
}));
});
const skillSlashMenuItems = computed<SlashMenuItem[]>(() =>
filteredSkills.value.map((skill) => ({
id: `skill:${skill.path}`,
@ -933,17 +1057,36 @@ const activeSlashMenuItems = computed<SlashMenuItem[]>(() => {
if (slashMenuMode.value === 'theme') {
return themeSlashMenuItems.value;
}
if (slashMenuMode.value === 'permission') {
return permissionSlashMenuItems.value;
}
if (slashMenuMode.value === 'execution') {
return executionSlashMenuItems.value;
}
if (slashMenuMode.value === 'model') {
return modelSlashMenuItems.value;
}
if (slashMenuMode.value === 'run-mode') {
return runModeSlashMenuItems.value;
}
return rootSlashMenuItems.value;
});
const slashMenuAriaLabel = computed(() => {
if (slashMenuMode.value === 'skills') return '可用 AgentSkills';
if (slashMenuMode.value === 'theme') return '主题选项';
if (slashMenuMode.value === 'permission') return '权限模式选项';
if (slashMenuMode.value === 'execution') return '执行环境选项';
if (slashMenuMode.value === 'model') return '模型选项';
if (slashMenuMode.value === 'run-mode') return '思考模式选项';
return '快捷指令';
});
const slashMenuEmptyText = computed(() => {
if (slashMenuMode.value === 'skills') return skillsLoading.value ? '正在加载 skills...' : '没有匹配的 skill';
if (slashMenuMode.value === 'permission') return '无可用权限模式';
if (slashMenuMode.value === 'execution') return '无可用执行环境';
if (slashMenuMode.value === 'model') return '无可用模型';
return '没有匹配的指令';
});

View File

@ -30,7 +30,7 @@
--runtime-queue-border: rgba(15, 23, 42, 0.12);
position: absolute;
left: 50%;
bottom: calc(100% - 2px);
bottom: calc(100% + 1px);
transform: translateX(-50%);
z-index: 1;
display: flex;
@ -128,16 +128,16 @@
}
.skill-slash-menu-wrapper {
--skill-slash-row-height: 38px;
--skill-slash-gap: 5px;
--skill-slash-radius: 12px;
--skill-slash-pad-x: 5px;
--skill-slash-row-height: 28px;
--skill-slash-gap: 3px;
--skill-slash-radius: 9px;
--skill-slash-pad-x: 4px;
--skill-slash-motion-duration: 300ms;
--skill-slash-motion-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
--skill-slash-visible-rows: 5;
--skill-slash-visible-rows: 7;
position: absolute;
left: 50%;
bottom: calc(100% - 2px);
bottom: calc(100% - 1px);
transform: translateX(-50%);
z-index: 1;
width: 90%;
@ -146,7 +146,7 @@
max-height: calc(
(var(--skill-slash-row-height) * var(--skill-slash-visible-rows)) +
(var(--skill-slash-gap) * (var(--skill-slash-visible-rows) + 1)) +
2px
2px + 2px
);
border: 1px solid var(--claude-border);
border-top-left-radius: var(--skill-slash-radius);
@ -187,7 +187,7 @@
max-height: inherit;
overflow-y: auto;
scrollbar-width: none;
padding: var(--skill-slash-gap) var(--skill-slash-pad-x);
padding: 4px var(--skill-slash-pad-x);
display: flex;
flex-direction: column;
gap: var(--skill-slash-gap);
@ -204,7 +204,7 @@
right: var(--skill-slash-pad-x);
top: var(--skill-slash-gap);
height: var(--skill-slash-row-height);
border-radius: 10px;
border-radius: 7px;
background: var(--hover-bg);
box-shadow: 0 1px 2px var(--shadow-color);
pointer-events: none;
@ -218,10 +218,10 @@
flex: 0 0 var(--skill-slash-row-height);
display: flex;
align-items: center;
gap: 10px;
gap: 7px;
border: none;
border-radius: 10px;
padding: 7px 12px;
border-radius: 7px;
padding: 3px 9px;
background: transparent;
color: var(--claude-text);
text-align: left;
@ -262,9 +262,9 @@
.skill-slash-item__name {
flex: 0 0 auto;
max-width: 210px;
max-width: 160px;
color: var(--accent);
font-size: 13px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
@ -287,9 +287,9 @@
flex: 0 0 var(--skill-slash-row-height);
display: flex;
align-items: center;
padding: 7px 12px;
padding: 3px 9px;
color: var(--claude-text-secondary);
font-size: 13px;
font-size: 11px;
}
body[data-theme='dark'] {