feat(refactor): live zed-style codebase files autocomplete and context attachment

This commit is contained in:
2026-05-21 17:20:31 -07:00
parent 8049a7f1ab
commit b3dd3714c3
5 changed files with 325 additions and 20 deletions

View File

@@ -882,6 +882,24 @@ export function ChatPanel({
const [chatMode, setChatMode] = useState<"collaborate" | "vibe" | "delegate">(
"vibe",
);
const [projectFiles, setProjectFiles] = useState<string[]>([]);
const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [suggestionsFilter, setSuggestionsFilter] = useState("");
const [suggestionIndex, setSuggestionIndex] = useState(0);
// Fetch codebase files list inside the scoped project
useEffect(() => {
if (!projectId || !workspace || status !== "authenticated") return;
fetch(`/api/projects/${projectId}/files?workspace=${workspace}`)
.then((res) => res.json())
.then((data) => {
if (Array.isArray(data.files)) {
setProjectFiles(data.files);
}
})
.catch(() => {});
}, [projectId, workspace, status]);
const [sending, setSending] = useState(false);
const [showThreads, setShowThreads] = useState(false);
const [mcpToken, setMcpToken] = useState<string | null>(null);
@@ -1149,6 +1167,7 @@ export function ChatPanel({
throw new Error(err.error || `HTTP ${r.status}`);
}
setAttachedFiles([]);
setMessages((prev) => [
...prev,
{
@@ -1170,10 +1189,13 @@ export function ChatPanel({
workspace,
mcp_token: mcpToken,
chatMode,
attachedFiles,
}),
signal: controller.signal,
});
setAttachedFiles([]);
if (!res.ok || !res.body) throw new Error("Stream failed");
const reader = res.body.getReader();
@@ -1389,6 +1411,7 @@ export function ChatPanel({
unifiedProjectShell,
chatMode,
projectId,
attachedFiles,
],
);
@@ -1419,13 +1442,57 @@ export function ChatPanel({
window.removeEventListener("vibn:chat-prompt", onPrompt as EventListener);
}, [sendMessage, projectId]);
const handleInputChange = (val: string) => {
setInput(val);
const match = val.match(/\/(\S*)$/);
if (match) {
setShowSuggestions(true);
setSuggestionsFilter(match[1] || "");
setSuggestionIndex(0);
} else {
setShowSuggestions(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
} else if (e.key === "Escape" && sending) {
e.preventDefault();
cancelMessage();
if (showSuggestions) {
const filtered = projectFiles
.filter((f) =>
f.toLowerCase().includes(suggestionsFilter.toLowerCase()),
)
.slice(0, 8);
if (e.key === "ArrowDown") {
e.preventDefault();
setSuggestionIndex((prev) => (prev + 1) % Math.max(1, filtered.length));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSuggestionIndex(
(prev) => (prev - 1 + filtered.length) % Math.max(1, filtered.length),
);
} else if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
if (filtered[suggestionIndex]) {
const selectedFile = filtered[suggestionIndex];
const before = input.slice(0, input.lastIndexOf("/"));
setInput(before + " ");
if (!attachedFiles.includes(selectedFile)) {
setAttachedFiles((prev) => [...prev, selectedFile]);
}
setShowSuggestions(false);
}
} else if (e.key === "Escape") {
e.preventDefault();
setShowSuggestions(false);
}
} else {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
} else if (e.key === "Escape" && sending) {
e.preventDefault();
cancelMessage();
}
}
};
@@ -1676,6 +1743,7 @@ export function ChatPanel({
borderTop: "1px solid #e8e4dc",
background: "#faf8f5",
flexShrink: 0,
position: "relative",
}}
>
{/* Chat Mode Toggle */}
@@ -1806,6 +1874,149 @@ export function ChatPanel({
Read-only mode add your MCP token in Settings to enable actions.
</div>
)}
{/* File Autocomplete Suggestions */}
{showSuggestions &&
(() => {
const filtered = projectFiles
.filter((f) =>
f.toLowerCase().includes(suggestionsFilter.toLowerCase()),
)
.slice(0, 8);
if (filtered.length === 0) return null;
return (
<div
style={{
position: "absolute",
bottom: "calc(100% - 10px)",
left: 14,
right: 14,
background: "#fff",
border: "1px solid #e8e4dc",
borderRadius: 8,
boxShadow:
"0 -4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.02)",
zIndex: 1000,
maxHeight: 200,
overflowY: "auto",
padding: "4px 0",
}}
>
<div
style={{
fontSize: "0.65rem",
fontWeight: 600,
color: "#a09a90",
padding: "6px 12px 4px",
borderBottom: "1px solid #f0ede8",
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
Codebase Files
</div>
{filtered.map((file, idx) => (
<div
key={file}
onClick={() => {
const before = input.slice(0, input.lastIndexOf("/"));
setInput(before + " ");
if (!attachedFiles.includes(file)) {
setAttachedFiles((prev) => [...prev, file]);
}
setShowSuggestions(false);
}}
onMouseEnter={() => setSuggestionIndex(idx)}
style={{
padding: "6px 12px",
fontSize: "0.76rem",
fontFamily: "var(--font-mono), monospace",
color: idx === suggestionIndex ? "#3d5afe" : "#1a1a1a",
background:
idx === suggestionIndex ? "#3d5afe0c" : "transparent",
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
📄 {file}
</span>
{idx === suggestionIndex && (
<span
style={{
fontSize: "0.6rem",
color: "#3d5afe",
opacity: 0.7,
fontFamily: "var(--font-inter), sans-serif",
}}
>
press Enter
</span>
)}
</div>
))}
</div>
);
})()}
{/* Attached Files Chips */}
{attachedFiles.length > 0 && (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 6,
marginBottom: 8,
}}
>
{attachedFiles.map((file) => (
<div
key={file}
style={{
display: "flex",
alignItems: "center",
gap: 6,
background: "#eef2ff",
border: "1px solid #c7d2fe",
borderRadius: 6,
padding: "4px 8px",
fontSize: "0.7rem",
color: "#312e81",
fontFamily: "var(--font-mono), monospace",
}}
>
<span>📄 {file.split("/").pop()}</span>
<button
type="button"
onClick={() =>
setAttachedFiles((prev) => prev.filter((f) => f !== file))
}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
color: "#4338ca",
display: "flex",
alignItems: "center",
}}
title="Remove file"
>
<X style={{ width: 12, height: 12 }} />
</button>
</div>
))}
</div>
)}
<ProjectPreviewChatInputWrap unifiedShell={unifiedProjectShell}>
{(selectToggle) => (
<div
@@ -1823,7 +2034,7 @@ export function ChatPanel({
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
sending ? "Esc to stop generating…" : "Ask Vibn AI anything…"