Solves #700 ## State of the World Before Prior to this PR, when users wanted to share file contents with Codex, they had two options: - Manually copy and paste file contents into the chat - Wait for the assistant to use the shell tool to view the file The second approach required the assistant to: 1. Recognize the need to view a file 2. Execute a shell tool call 3. Wait for the tool call to complete 4. Process the file contents This consumed extra tokens and reduced user control over which files were shared with the model. ## State of the World After With this PR, users can now: - Reference files directly in their chat input using the `@path` syntax - Have file contents automatically expanded into XML blocks before being sent to the LLM For example, users can type `@src/utils/config.js` in their message, and the file contents will be included in context. Within the terminal chat history, these file blocks will be collapsed back to `@path` format in the UI for clean presentation. Tag File suggestions: <img width="857" alt="file-suggestions" src="https://github.com/user-attachments/assets/397669dc-ad83-492d-b5f0-164fab2ff4ba" /> Tagging files in action: <img width="858" alt="tagging-files" src="https://github.com/user-attachments/assets/0de9d559-7b7f-4916-aeff-87ae9b16550a" /> Demo video of file tagging: [](https://www.youtube.com/watch?v=vL4LqtBnqt8) ## Implementation Details This PR consists of 2 main components: 1. **File Tag Utilities**: - New `file-tag-utils.ts` utility module that handles both expansion and collapsing of file tags - `expandFileTags()` identifies `@path` tokens and replaces them with XML blocks containing file contents - `collapseXmlBlocks()` reverses the process, converting XML blocks back to `@path` format for UI display - Tokens are only expanded if they point to valid files (directories are ignored) - Expansion happens just before sending input to the model 2. **Terminal Chat Integration**: - Leveraged the existing file system completion system for tabbing to support the `@path` syntax - Added `updateFsSuggestions` helper to manage filesystem suggestions - Added `replaceFileSystemSuggestion` to replace input with filesystem suggestions - Applied `collapseXmlBlocks` in the chat response rendering so that tagged files are shown as simple `@path` tags The PR also includes test coverage for both the UI and the file tag utilities. ## Next Steps Some ideas I'd like to implement if this feature gets merged: - Line selection: `@path[50:80]` to grab specific sections of files - Method selection: `@path#methodName` to grab just one function/class - Visual improvements: highlight file tags in the UI to make them more noticeable
93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import { getFileSystemSuggestions } from "../src/utils/file-system-suggestions";
|
|
|
|
vi.mock("fs");
|
|
vi.mock("os");
|
|
|
|
describe("getFileSystemSuggestions", () => {
|
|
const mockFs = fs as unknown as {
|
|
readdirSync: ReturnType<typeof vi.fn>;
|
|
statSync: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
const mockOs = os as unknown as {
|
|
homedir: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("returns empty array for empty prefix", () => {
|
|
expect(getFileSystemSuggestions("")).toEqual([]);
|
|
});
|
|
|
|
it("expands ~ to home directory", () => {
|
|
mockOs.homedir = vi.fn(() => "/home/testuser");
|
|
mockFs.readdirSync = vi.fn(() => ["file1.txt", "docs"]);
|
|
mockFs.statSync = vi.fn((p) => ({
|
|
isDirectory: () => path.basename(p) === "docs",
|
|
}));
|
|
|
|
const result = getFileSystemSuggestions("~/");
|
|
|
|
expect(mockFs.readdirSync).toHaveBeenCalledWith("/home/testuser");
|
|
expect(result).toEqual([
|
|
{
|
|
path: path.join("/home/testuser", "file1.txt"),
|
|
isDirectory: false,
|
|
},
|
|
{
|
|
path: path.join("/home/testuser", "docs" + path.sep),
|
|
isDirectory: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("filters by prefix if not a directory", () => {
|
|
mockFs.readdirSync = vi.fn(() => ["abc.txt", "abd.txt", "xyz.txt"]);
|
|
mockFs.statSync = vi.fn((p) => ({
|
|
isDirectory: () => p.includes("abd"),
|
|
}));
|
|
|
|
const result = getFileSystemSuggestions("a");
|
|
expect(result).toEqual([
|
|
{
|
|
path: "abc.txt",
|
|
isDirectory: false,
|
|
},
|
|
{
|
|
path: "abd.txt/",
|
|
isDirectory: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("handles errors gracefully", () => {
|
|
mockFs.readdirSync = vi.fn(() => {
|
|
throw new Error("failed");
|
|
});
|
|
|
|
const result = getFileSystemSuggestions("some/path");
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("normalizes relative path", () => {
|
|
mockFs.readdirSync = vi.fn(() => ["foo", "bar"]);
|
|
mockFs.statSync = vi.fn((_p) => ({
|
|
isDirectory: () => true,
|
|
}));
|
|
|
|
const result = getFileSystemSuggestions("./");
|
|
const paths = result.map((item) => item.path);
|
|
const allDirectories = result.every((item) => item.isDirectory === true);
|
|
|
|
expect(paths).toContain("foo/");
|
|
expect(paths).toContain("bar/");
|
|
expect(allDirectories).toBe(true);
|
|
});
|
|
});
|