diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index 99df796..7a758d0 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -535,7 +535,7 @@ const stadiumInput = ref(null); const fileUploadInput = ref(null); const baselineComposerVisualHeight = ref(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(null); const slashMenuMode = ref('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(() => { 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(() => { })); }); +const permissionSlashMenuItems = computed(() => { + 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(() => { + 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(() => { + 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(() => { + 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(() => filteredSkills.value.map((skill) => ({ id: `skill:${skill.path}`, @@ -933,17 +1057,36 @@ const activeSlashMenuItems = computed(() => { 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 '没有匹配的指令'; }); @@ -1866,7 +2009,7 @@ const emitComposerHeight = () => { } const baseline = baselineComposerVisualHeight.value || visualHeight; const growth = Math.max(0, visualHeight - baseline); - const baseReservedHeight = 90; + const baseReservedHeight = 93; const reservedHeight = Math.ceil(baseReservedHeight + growth); emit('composer-height-change', { reservedHeight, diff --git a/static/src/styles/components/input/_composer.scss b/static/src/styles/components/input/_composer.scss index fb6711f..8e64e5d 100644 --- a/static/src/styles/components/input/_composer.scss +++ b/static/src/styles/components/input/_composer.scss @@ -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 - 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) 6px; 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'] {