Skip to main content
Glama
MIT License
27,120
19,789
  • Linux
  • Apple
TryItFileSelection.vue8.79 kB
<template> <div class="file-selection-container"> <div class="file-selection-header"> <h3 class="file-selection-title"> <FileText :size="16" class="title-icon" /> File Selection </h3> <div class="file-selection-actions"> <button type="button" class="action-btn select-all" @click="selectAll" :disabled="!hasFiles" aria-label="Select all files" > Select All </button> <button type="button" class="action-btn deselect-all" @click="deselectAll" :disabled="!hasFiles" aria-label="Deselect all files" > Deselect All </button> <button type="button" class="action-btn repack" @click="handleRepack" :disabled="!hasSelectedFiles || loading" :aria-label="loading ? 'Re-packing selected files' : `Re-pack ${selectedFiles.length} selected files`" > {{ loading ? 'Re-packing...' : 'Re-pack Selected' }} <PackIcon v-if="!loading" :size="14" /> </button> </div> </div> <div class="file-selection-stats"> <span class="stat-item"> {{ selectedFiles.length }} of {{ allFiles.length }} files selected </span> <span class="stat-separator">|</span> <span class="stat-item"> {{ selectedTokens.toLocaleString() }} tokens ({{ totalTokens > 0 ? ((selectedTokens / totalTokens) * 100).toFixed(1) : '0.0' }}%) </span> </div> <div class="file-list-container"> <div class="file-list-scroll"> <table class="file-table" aria-label="File selection table"> <thead> <tr> <th class="checkbox-column"> <input type="checkbox" :checked="selectedFiles.length === allFiles.length && allFiles.length > 0" :indeterminate="selectedFiles.length > 0 && selectedFiles.length < allFiles.length" @change="($event.target as HTMLInputElement).checked ? selectAll() : deselectAll()" class="header-checkbox" aria-label="Select or deselect all files" /> </th> <th class="file-path-column">File Path</th> <th class="tokens-column">Tokens</th> </tr> </thead> <tbody> <tr v-for="file in sortedFiles" :key="file.path" class="file-row" :class="{ 'file-row-selected': file.selected }" @click="toggleFileSelection(file, $event)" > <td class="checkbox-cell"> <input type="checkbox" v-model="file.selected" class="file-checkbox" :aria-label="`Select file ${file.path}`" /> </td> <td class="file-path-cell"> <span class="file-path">{{ file.path }}</span> </td> <td class="tokens-cell"> <span class="file-tokens">{{ file.tokenCount.toLocaleString() }}</span> </td> </tr> </tbody> </table> </div> </div> <FileSelectionWarning v-if="selectedFiles.length > FILE_SELECTION_WARNING_THRESHOLD" :threshold="FILE_SELECTION_WARNING_THRESHOLD" /> </div> </template> <script setup lang="ts"> import { FileText } from 'lucide-vue-next'; import { computed, ref, watch } from 'vue'; import { FILE_SELECTION_WARNING_THRESHOLD } from '../../constants/fileSelection'; import type { FileInfo } from '../api/client'; import FileSelectionWarning from './FileSelectionWarning.vue'; import PackIcon from './PackIcon.vue'; interface Props { allFiles: FileInfo[]; loading?: boolean; } type Emits = (e: 'repack', selectedFiles: FileInfo[]) => void; const props = withDefaults(defineProps<Props>(), { loading: false, }); const emit = defineEmits<Emits>(); // Local reactive state to avoid mutating props directly (Vue one-way data flow) const localFiles = ref<FileInfo[]>([]); // Sync props.allFiles to localFiles with deep copying to maintain reactivity watch( () => props.allFiles, (newFiles) => { // Deep clone to avoid mutating props - fallback to JSON method for compatibility try { localFiles.value = structuredClone(newFiles || []); } catch { localFiles.value = JSON.parse(JSON.stringify(newFiles || [])); } }, { immediate: true }, ); const hasFiles = computed(() => localFiles.value.length > 0); const selectedFiles = computed(() => localFiles.value.filter((file) => file.selected)); const hasSelectedFiles = computed(() => selectedFiles.value.length > 0); const totalTokens = computed(() => localFiles.value.reduce((sum, file) => sum + file.tokenCount, 0)); const selectedTokens = computed(() => selectedFiles.value.reduce((sum, file) => sum + file.tokenCount, 0)); const sortedFiles = computed(() => [...localFiles.value].sort((a, b) => b.tokenCount - a.tokenCount)); const selectAll = () => { for (const file of localFiles.value) { file.selected = true; } }; const deselectAll = () => { for (const file of localFiles.value) { file.selected = false; } }; const handleRepack = () => { if (hasSelectedFiles.value) { emit('repack', selectedFiles.value); } }; const toggleFileSelection = (file: FileInfo, event?: Event) => { // Prevent double-toggling when clicking directly on checkbox if (event?.target && (event.target as HTMLInputElement).type === 'checkbox') { return; } file.selected = !file.selected; }; </script> <style scoped> .file-selection-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid var(--vp-c-border); background: var(--vp-c-bg-soft); border-radius: 8px 8px 0 0; } .file-selection-title { margin: 0; font-size: 16px; font-weight: 600; color: var(--vp-c-text-1); display: flex; align-items: center; gap: 8px; } .title-icon { color: var(--vp-c-text-2); } .file-selection-actions { display: flex; gap: 8px; } .action-btn { padding: 6px 12px; border: 1px solid var(--vp-c-border); border-radius: 4px; background: var(--vp-c-bg); color: var(--vp-c-text-1); font-size: 13px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px; } .action-btn:hover:not(:disabled) { background: var(--vp-c-bg-soft); border-color: var(--vp-c-brand-1); } .action-btn.repack { background: var(--vp-c-brand-1); color: white; border-color: var(--vp-c-brand-1); } .action-btn.repack:hover:not(:disabled) { background: var(--vp-c-brand-2); } .action-btn:disabled { opacity: 0.5; cursor: not-allowed; } .file-selection-stats { padding: 12px 16px; background: var(--vp-c-bg-alt); border-bottom: 1px solid var(--vp-c-border); font-size: 13px; color: var(--vp-c-text-2); } .stat-item { color: var(--vp-c-text-1); } .stat-separator { margin: 0 8px; color: var(--vp-c-text-3); } .file-list-container { max-height: 300px; overflow-y: auto; background: var(--vp-c-bg); } .file-list-scroll { padding: 0; } .file-table { width: 100%; border-collapse: collapse; } .file-table th { background: var(--vp-c-bg-soft); border-bottom: 1px solid var(--vp-c-border); padding: 8px 12px; text-align: left; font-size: 12px; font-weight: 600; color: var(--vp-c-text-2); position: sticky; top: 0; } .checkbox-column { width: 40px; text-align: center; } .file-path-column { width: 70%; } .file-table .tokens-column { width: 30%; text-align: left; padding-right: 2rem; } .file-row { transition: background-color 0.2s ease; cursor: pointer; } .file-row:hover { background: var(--vp-c-bg-soft); } .file-table td { padding: 8px 12px; border-bottom: 1px solid var(--vp-c-border-soft); } .checkbox-cell { text-align: center; } .file-path-cell { max-width: 0; width: 100%; } .file-path { font-size: 13px; color: var(--vp-c-text-1); word-break: break-all; font-family: var(--vp-font-family-mono); } .file-table .tokens-cell { text-align: left; padding-right: 2rem; } .file-tokens { font-size: 12px; color: var(--vp-c-text-2); white-space: nowrap; } .header-checkbox { cursor: pointer; } .file-checkbox { cursor: pointer; } @media (max-width: 768px) { .file-selection-header { flex-direction: column; gap: 12px; align-items: stretch; } .file-selection-actions { justify-content: space-between; } .action-btn { flex: 1; justify-content: center; } } </style>

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/yamadashy/repomix'

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