feat(refactor): live zed-style codebase files autocomplete and context attachment
This commit is contained in:
@@ -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…"
|
||||
|
||||
Reference in New Issue
Block a user