@@ -104,6 +105,7 @@ import { FolderArchive, FolderOpen, Link2, RotateCcw } from 'lucide-vue-next';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { usePackRequest } from '../../composables/usePackRequest';
import { hasNonDefaultValues, parseUrlParameters, updateUrlParameters } from '../../utils/urlParams';
+import type { FileInfo } from '../api/client';
import { isValidRemoteValue } from '../utils/validation';
import PackButton from './PackButton.vue';
import TryItFileUpload from './TryItFileUpload.vue';
@@ -137,6 +139,7 @@ const {
setMode,
handleFileUpload,
submitRequest,
+ repackWithSelectedFiles,
resetOptions,
} = usePackRequest();
@@ -175,7 +178,20 @@ function updateUrlFromCurrentState() {
updateUrlParameters(urlParamsToUpdate);
}
-async function handleSubmit() {
+async function handleSubmit(event?: SubmitEvent) {
+ // Prevent accidental form submissions from unintended buttons
+ if (event?.submitter && !isSubmitValid.value) {
+ const submitter = event.submitter as HTMLElement;
+ if (!submitter.matches('.pack-button, [type="submit"]')) {
+ return; // Ignore submission from non-pack buttons when form is invalid
+ }
+ }
+
+ // Only proceed if form is valid
+ if (!isSubmitValid.value) {
+ return;
+ }
+
await submitRequest();
}
@@ -193,6 +209,10 @@ function handleReset() {
updateUrlParameters({});
}
+function handleRepack(selectedFiles: FileInfo[]) {
+ repackWithSelectedFiles(selectedFiles);
+}
+
// Watch for changes in packOptions and inputUrl to update URL in real-time
watch(
[packOptions, inputUrl],
diff --git a/website/client/components/Home/TryItFileSelection.vue b/website/client/components/Home/TryItFileSelection.vue
new file mode 100644
index 000000000..2de0ef3f9
--- /dev/null
+++ b/website/client/components/Home/TryItFileSelection.vue
@@ -0,0 +1,373 @@
+
+
+
+
+
+
+ {{ selectedFiles.length }} of {{ allFiles.length }} files selected
+
+ |
+
+ {{ selectedTokens.toLocaleString() }} tokens
+ ({{ totalTokens > 0 ? ((selectedTokens / totalTokens) * 100).toFixed(1) : '0.0' }}%)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/client/components/Home/TryItLoading.vue b/website/client/components/Home/TryItLoading.vue
new file mode 100644
index 000000000..a13a082c1
--- /dev/null
+++ b/website/client/components/Home/TryItLoading.vue
@@ -0,0 +1,77 @@
+
+
+
+
Processing repository...
+
+
+
+
+
\ No newline at end of file
diff --git a/website/client/components/Home/TryItResult.vue b/website/client/components/Home/TryItResult.vue
index 08b0a66e4..9e3fd8c6d 100644
--- a/website/client/components/Home/TryItResult.vue
+++ b/website/client/components/Home/TryItResult.vue
@@ -1,50 +1,91 @@
-
-
-
Processing repository...
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
@@ -56,56 +97,39 @@ defineProps<{
overflow: hidden;
}
-.loading {
- padding: 36px;
- text-align: center;
-}
-
-.spinner {
- width: 40px;
- height: 40px;
- margin: 0 auto 16px;
- border: 3px solid var(--vp-c-brand-1);
- border-radius: 50%;
- border-top-color: transparent;
- animation: spin 1s linear infinite;
-}
-
-.sponsor-section {
- margin-top: 16px;
+.result-content {
display: flex;
flex-direction: column;
- align-items: center;
-}
-
-.sponsor-section p {
- margin: 8px 0;
}
-.sponsor-section .sponsor-header {
- font-size: 0.9em;
+.tab-navigation {
+ display: flex;
+ border-bottom: 1px solid var(--vp-c-border);
+ background: var(--vp-c-bg-soft);
}
-.sponsor-section img {
- max-width: 100%;
- height: auto;
- margin: 12px 0;
+.tab-button {
+ flex: 1;
+ padding: 12px 16px;
+ border: none;
+ background: transparent;
+ color: var(--vp-c-text-2);
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border-bottom: 2px solid transparent;
}
-.sponsor-section .sponsor-title {
- font-weight: bold;
- font-size: 1.1em;
- color: var(--vp-c-brand-1);
- text-decoration: underline;
+.tab-button:hover {
+ background: var(--vp-c-bg-alt);
+ color: var(--vp-c-text-1);
}
-.sponsor-section .sponsor-subtitle {
- font-size: 0.9em;
+.tab-button.active {
color: var(--vp-c-brand-1);
- text-decoration: underline;
+ border-bottom-color: var(--vp-c-brand-1);
+ background: var(--vp-c-bg);
}
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
diff --git a/website/client/components/api/client.ts b/website/client/components/api/client.ts
index 3c8a6f92e..f2e3f3fde 100644
--- a/website/client/components/api/client.ts
+++ b/website/client/components/api/client.ts
@@ -10,6 +10,13 @@ export interface PackOptions {
compress?: boolean;
}
+export interface FileInfo {
+ path: string;
+ charCount: number;
+ tokenCount: number;
+ selected?: boolean;
+}
+
export interface PackRequest {
url: string;
format: 'xml' | 'markdown' | 'plain';
@@ -32,7 +39,9 @@ export interface PackResult {
topFiles: {
path: string;
charCount: number;
+ tokenCount: number;
}[];
+ allFiles?: FileInfo[];
};
}
diff --git a/website/client/composables/usePackOptions.ts b/website/client/composables/usePackOptions.ts
index b9f39f824..c7292fa79 100644
--- a/website/client/composables/usePackOptions.ts
+++ b/website/client/composables/usePackOptions.ts
@@ -35,7 +35,8 @@ export function usePackOptions() {
for (const key of Object.keys(packOptions) as Array
) {
if (key in urlParams && urlParams[key] !== undefined) {
// Type-safe assignment: only assign if the key is a valid PackOptions key
- packOptions[key] = urlParams[key] as PackOptions[typeof key];
+ // @ts-expect-error
+ packOptions[key] = urlParams[key];
}
}
}
diff --git a/website/client/composables/usePackRequest.ts b/website/client/composables/usePackRequest.ts
index 3eb8a9174..36e084d75 100644
--- a/website/client/composables/usePackRequest.ts
+++ b/website/client/composables/usePackRequest.ts
@@ -1,5 +1,5 @@
import { computed, onMounted, ref } from 'vue';
-import type { PackResult } from '../components/api/client';
+import type { FileInfo, PackResult } from '../components/api/client';
import { handlePackRequest } from '../components/utils/requestHandlers';
import { isValidRemoteValue } from '../components/utils/validation';
import { parseUrlParameters } from '../utils/urlParams';
@@ -99,6 +99,37 @@ export function usePackRequest() {
}
}
+ async function repackWithSelectedFiles(selectedFiles: FileInfo[]) {
+ if (!result.value || selectedFiles.length === 0) return;
+
+ // Generate include patterns from selected files
+ const selectedPaths = selectedFiles.map((file) => file.path);
+ const includePatterns = selectedPaths.join(',');
+
+ // Temporarily update pack options with include patterns
+ const originalIncludePatterns = packOptions.includePatterns;
+ const originalIgnorePatterns = packOptions.ignorePatterns;
+
+ packOptions.includePatterns = includePatterns;
+ packOptions.ignorePatterns = ''; // Clear ignore patterns to ensure selected files are included
+
+ try {
+ // Use the same loading state as normal pack processing
+ await submitRequest();
+
+ // Update file selection state in the new result
+ if (result.value?.metadata?.allFiles) {
+ for (const file of result.value.metadata.allFiles) {
+ file.selected = selectedPaths.includes(file.path);
+ }
+ }
+ } finally {
+ // Restore original pack options
+ packOptions.includePatterns = originalIncludePatterns;
+ packOptions.ignorePatterns = originalIgnorePatterns;
+ }
+ }
+
function cancelRequest() {
if (requestController) {
requestController.abort();
@@ -147,6 +178,7 @@ export function usePackRequest() {
handleFileUpload,
resetRequest,
submitRequest,
+ repackWithSelectedFiles,
cancelRequest,
// Pack option actions
diff --git a/website/client/composables/useZipProcessor.ts b/website/client/composables/useZipProcessor.ts
index 64a3104b3..23d127747 100644
--- a/website/client/composables/useZipProcessor.ts
+++ b/website/client/composables/useZipProcessor.ts
@@ -16,7 +16,7 @@ export function useZipProcessor() {
if (err) {
reject(new Error(`Failed to create ZIP file: ${err.message}`));
} else {
- const zipBlob = new Blob([data], { type: 'application/zip' });
+ const zipBlob = new Blob([data as BlobPart], { type: 'application/zip' });
resolve(new File([zipBlob], `${folderName}.zip`, { type: 'application/zip' }));
}
});
diff --git a/website/client/constants/fileSelection.ts b/website/client/constants/fileSelection.ts
new file mode 100644
index 000000000..bf6d2f4fb
--- /dev/null
+++ b/website/client/constants/fileSelection.ts
@@ -0,0 +1 @@
+export const FILE_SELECTION_WARNING_THRESHOLD = 500;
diff --git a/website/client/tsconfig.json b/website/client/tsconfig.json
index fc72973a9..109f524b5 100644
--- a/website/client/tsconfig.json
+++ b/website/client/tsconfig.json
@@ -16,6 +16,16 @@
"baseUrl": ".",
"types": ["vite/client", "vitepress/client"]
},
- "include": [".vitepress/**/*.ts", ".vitepress/**/*.d.ts", ".vitepress/**/*.tsx", ".vitepress/**/*.vue"],
+ "include": [
+ ".vitepress/**/*.ts",
+ ".vitepress/**/*.d.ts",
+ ".vitepress/**/*.tsx",
+ ".vitepress/**/*.vue",
+ "components/**/*.ts",
+ "components/**/*.vue",
+ "composables/**/*.ts",
+ "utils/**/*.ts",
+ "constants/**/*.ts"
+ ],
"exclude": ["node_modules", "dist"]
}
diff --git a/website/client/types/ui.ts b/website/client/types/ui.ts
new file mode 100644
index 000000000..fa4e9b9c1
--- /dev/null
+++ b/website/client/types/ui.ts
@@ -0,0 +1 @@
+export type TabType = 'result' | 'files';
diff --git a/website/server/package-lock.json b/website/server/package-lock.json
index a34d620eb..0b31601f8 100644
--- a/website/server/package-lock.json
+++ b/website/server/package-lock.json
@@ -10,7 +10,7 @@
"@hono/node-server": "^1.16.0",
"fflate": "^0.8.2",
"hono": "^4.8.5",
- "repomix": "^1.2.0",
+ "repomix": "^1.4.0",
"winston": "^3.17.0",
"zod": "^3.24.1"
},
@@ -3380,9 +3380,9 @@
}
},
"node_modules/repomix": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/repomix/-/repomix-1.2.0.tgz",
- "integrity": "sha512-rPUE73lQOAFu32O+rT6rFHxQlfwKJJF9JrQh+aOeSBSfWwu52l/+q/Qj25x2S3TVaWK+3j/FeVyFs6hoCjsOhw==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/repomix/-/repomix-1.4.0.tgz",
+ "integrity": "sha512-n3Yzmbh4AonDIJ6eRjySIJ5fnUD5LxmgAMqB6B2UWsfhzuZbNS8/LwhcgUtMEREmJlUjifQWTPJ+6x27lMorJQ==",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^0.10.1",
diff --git a/website/server/package.json b/website/server/package.json
index 2791f3b23..c56d6749a 100644
--- a/website/server/package.json
+++ b/website/server/package.json
@@ -16,7 +16,7 @@
"@hono/node-server": "^1.16.0",
"fflate": "^0.8.2",
"hono": "^4.8.5",
- "repomix": "^1.2.0",
+ "repomix": "^1.4.0",
"winston": "^3.17.0",
"zod": "^3.24.1"
},
diff --git a/website/server/src/remoteRepo.ts b/website/server/src/remoteRepo.ts
index 90b2140be..fbb84d2cb 100644
--- a/website/server/src/remoteRepo.ts
+++ b/website/server/src/remoteRepo.ts
@@ -63,6 +63,7 @@ export async function processRemoteRepo(
topFilesLen: 10,
include: sanitizedIncludePatterns,
ignore: sanitizedIgnorePatterns,
+ tokenCountTree: true, // Required to generate token counts for all files in the repository
quiet: true, // Enable quiet mode to suppress output
} as CliOptions;
@@ -98,11 +99,18 @@ export async function processRemoteRepo(
topFiles: Object.entries(packResult.fileCharCounts)
.map(([path, charCount]) => ({
path,
- charCount,
+ charCount: charCount as number,
tokenCount: packResult.fileTokenCounts[path] || 0,
}))
- .sort((a, b) => b.charCount - a.charCount)
+ .sort((a, b) => b.tokenCount - a.tokenCount)
.slice(0, cliOptions.topFilesLen),
+ allFiles: Object.entries(packResult.fileTokenCounts)
+ .map(([path]) => ({
+ path,
+ tokenCount: packResult.fileTokenCounts[path] || 0,
+ selected: true, // Default to selected for initial packing
+ }))
+ .sort((a, b) => b.tokenCount - a.tokenCount),
},
};
diff --git a/website/server/src/schemas/request.ts b/website/server/src/schemas/request.ts
index 1ef57fdfd..5e4bda35d 100644
--- a/website/server/src/schemas/request.ts
+++ b/website/server/src/schemas/request.ts
@@ -14,7 +14,7 @@ export const packOptionsSchema = z
directoryStructure: z.boolean().optional(),
includePatterns: z
.string()
- .max(1000, 'Include patterns too long')
+ .max(100_000, 'Include patterns too long')
.optional()
.transform((val) => val?.trim()),
ignorePatterns: z
diff --git a/website/server/src/types.ts b/website/server/src/types.ts
index 066dc8750..e376e7008 100644
--- a/website/server/src/types.ts
+++ b/website/server/src/types.ts
@@ -16,6 +16,12 @@ interface TopFile {
tokenCount: number;
}
+interface FileInfo {
+ path: string;
+ tokenCount: number;
+ selected?: boolean;
+}
+
interface PackSummary {
totalFiles: number;
totalCharacters: number;
@@ -30,6 +36,7 @@ export interface PackResult {
timestamp: string;
summary?: PackSummary;
topFiles?: TopFile[];
+ allFiles?: FileInfo[];
};
}
diff --git a/website/server/src/utils/sharedInstance.ts b/website/server/src/utils/sharedInstance.ts
index bd07da9d4..463b16b05 100644
--- a/website/server/src/utils/sharedInstance.ts
+++ b/website/server/src/utils/sharedInstance.ts
@@ -4,4 +4,4 @@ import { RateLimiter } from './rateLimit.js';
// Create shared instances
export const cache = new RequestCache(180); // 3 minutes cache
-export const rateLimiter = new RateLimiter(60_000, 3); // 3 requests per minute
+export const rateLimiter = new RateLimiter(60_000, process.env.NODE_ENV === 'development' ? 10 : 3); // 10 requests per minute in dev, 3 in production