Skip to main content
Glama
CommandBar.vue8 kB
<template> <ol id="construction" class="flex flex-row w-full mb-md border border-neutral-600 p-sm" > <template v-for="(cmd, index) in commandBuffer" :key="`${cmd}-${index}`"> <li class="cmd buffer mr-xs text-md rounded-md bg-action-900 px-xs py-[.33rem]" @click="() => remove(cmd, index)" > {{ cmd.name }} </li> <li v-for="(choice, indexChoice) in cmd.choices" :key="`${choice.value}-${indexChoice}`" :class=" clsx( 'buffer mr-xs text-md rounded-md bg-action-500 px-xs py-[.33rem]', indexChoice + 1 === cmd.choices.length ? 'last' : '', ) " @click=" () => indexChoice + 1 === cmd.choices.length ? removeChoice(cmd, index, choice, indexChoice) : null " > {{ choice.label }} </li> </template> <li v-if="!maybeDone" class="text-lg grow relative"> <input id="commandInput" ref="commandInputRef" v-model="commandInput" autocomplete="off" spellcheck="false" role="presentation" placeholder="Type your command..." class="w-full" @keydown.enter="input" @keydown="filter" /> <ul v-if="dropDownOptions.length > 0" id="dropdown" ref="dropdown" :style="dropdownStyle" :class=" clsx( 'absolute top-8 left-0 text-sm', 'rounded-sm min-h-[15vh] max-h-[30vh] min-w-[10vh] max-w-[15vh] overflow-y-auto', themeClasses('bg-white text-black', 'bg-neutral-900 text-white'), ) " > <li v-for="option in filteredDropDownOptions" :key="option.value" class="p-xs" :class=" clsx( selectedOption?.value === option.value ? themeClasses('text-action-500', 'text-action-300') : '', selectedOption?.value === option.value ? themeClasses('bg-neutral-300', 'bg-neutral-600') : '', ) " @click="chooseArg(option)" > {{ option.label }} </li> <li v-if="filteredDropDownOptions.length === 0" class="ps-x italic"> No options </li> </ul> </li> <li v-else> <VButton ref="go" tone="action" variant="solid" size="sm" @keydown.enter="runGo" @click="runGo" > go </VButton> </li> </ol> </template> <script lang="ts" setup> import clsx from "clsx"; import { ref, watch, computed, toRaw, ComputedRef, Ref } from "vue"; import { VButton, themeClasses } from "@si/vue-lib/design-system"; import { Command, Option } from "@/shared/command"; const go = ref<InstanceType<typeof VButton>>(); const commandInputRef = ref<InstanceType<typeof HTMLInputElement>>(); const commandInput = ref<string>(""); const dropdown = ref<InstanceType<typeof HTMLUListElement>>(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const dropdownStyle = ref<any>({}); interface Props { commands: Command[]; setDropDown: ( command: ComputedRef<Command | undefined>, dropdownOptions: Ref<Option[]>, ) => void; } const props = defineProps<Props>(); const emit = defineEmits<{ (e: "done"): void; }>(); const reset = () => { commandBuffer.value = []; dropDownOptions.value = []; dropDownFilter.value = null; selectedOption.value = null; }; defineExpose({ reset, }); watch(go, (newGo, oldGo) => { // whenever newGo enters the stage, focus it so user can hit enter if (!oldGo && newGo) { newGo.focus(); } }); const commandBuffer = ref<Command[]>([]); const lastCmd = computed<Command | undefined>( () => commandBuffer.value[commandBuffer.value.length - 1], ); const dropDownOptions = ref<Option[]>([]); const dropDownFilter = ref<string | null>(); const selectedOption = ref<Option | null>(); const filter = (event: KeyboardEvent) => { let idx = filteredDropDownOptions.value.findIndex( (o) => o.value === selectedOption.value?.value, ); if (event.code === "ArrowDown") { if (idx === null) idx = 0; else { idx++; if (idx >= filteredDropDownOptions.value.length) idx = 0; } selectedOption.value = filteredDropDownOptions.value[idx]; } else if (event.code === "ArrowUp") { if (idx === null) idx = filteredDropDownOptions.value.length - 1; else { idx--; if (idx <= 0) idx = filteredDropDownOptions.value.length - 1; } selectedOption.value = filteredDropDownOptions.value[idx]; } else { // assuming letters, ignore enter, etc dropDownFilter.value = commandInput.value; } }; const filteredDropDownOptions = computed(() => { const filter = dropDownFilter.value?.trim().toLocaleLowerCase(); if (!filter || filter.length === 0) return dropDownOptions.value; return dropDownOptions.value.filter((o) => { return o.label.toLocaleLowerCase().includes(filter); }); }); const chooseArg = (choice: Option) => { lastCmd.value?.choices.push(choice); input(); }; const remove = (cmd: Command, index: number) => { commandBuffer.value.splice(index, 1); dropDownOptions.value = []; }; const removeChoice = ( cmd: Command, index: number, choice: Option, indexChoice: number, ) => { cmd.choices.splice(indexChoice, 1); props.setDropDown(lastCmd, dropDownOptions); }; const maybeDone = computed( () => lastCmd.value && lastCmd.value.expects.length === lastCmd.value.choices.length, ); watch(maybeDone, (newDone) => { if (newDone) dropDownOptions.value = []; }); const runGo = async () => { if (!maybeDone.value) return; else { await lastCmd.value?.execute(); reset(); emit("done"); } }; const input = (event?: KeyboardEvent) => { event?.preventDefault(); const str = commandInput.value; const numChoices = lastCmd.value?.choices.length ?? 0; // am i setting a choice for an arg? if (lastCmd.value) { if (selectedOption.value) { lastCmd.value.choices.push(structuredClone(toRaw(selectedOption.value))); selectedOption.value = null; // are there more left? set drop down for the next choice if (!maybeDone.value) { props.setDropDown(lastCmd, dropDownOptions); } } else if (lastCmd.value.expects.at(numChoices) === "stringInput") { // typing free text, not a dropdown choice if (event?.key === "Enter") { if (str) lastCmd.value.choices.push({ label: str, value: str }); } } else if (lastCmd.value.expects.length !== numChoices) { let choice = dropDownOptions.value.find((o) => o.label === str); if (!choice) choice = dropDownOptions.value.find((o) => o.value === str); if (choice) lastCmd.value.choices.push(structuredClone(choice)); // are there more left? set drop down for the next choice if (!maybeDone.value) { props.setDropDown(lastCmd, dropDownOptions); } } } // am i starting a new command? if (commandBuffer.value.length === 0) { if (!str) return; for (const cmd of props.commands) { if (str === cmd.name || str === cmd.shortcut) { commandBuffer.value.push(cmd.factory()); props.setDropDown(lastCmd, dropDownOptions); } } } // reset input if (commandInput.value) commandInput.value = ""; // TODO are we done? go! if (maybeDone.value) { go.value?.focus(); } else if (commandInput.value) { // keep focus commandInputRef.value?.focus(); } }; </script> <style lang="less"> #commandInput { outline: 0; background-color: transparent; } #construction .buffer { cursor: pointer; } #construction .cmd.buffer, #construction .last.buffer { cursor: not-allowed; } #dropdown { border: 1px solid #ccc; border-top: 0; } #dropdown > * { cursor: pointer; border-top: 1px solid #ccc; } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server