Clean up previous project display

This commit is contained in:
Dave
2026-02-16 19:53:31 +00:00
parent 8ed40dd444
commit e6638a6517
4 changed files with 96 additions and 28 deletions

View File

@@ -28,15 +28,18 @@ function renderHighlightedMatch(text: string, query: string) {
if (!query) return text; if (!query) return text;
let qIndex = 0; let qIndex = 0;
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
return text.split("").map((char, index) => { const counts = new Map<string, number>();
return text.split("").map((char) => {
const isMatch = const isMatch =
qIndex < lowerQuery.length && char.toLowerCase() === lowerQuery[qIndex]; qIndex < lowerQuery.length && char.toLowerCase() === lowerQuery[qIndex];
if (isMatch) { if (isMatch) {
qIndex += 1; qIndex += 1;
} }
const count = counts.get(char) ?? 0;
counts.set(char, count + 1);
return ( return (
<span <span
key={`${char}-${index}`} key={`${char}-${count}`}
style={isMatch ? { fontWeight: 600, color: "#222" } : undefined} style={isMatch ? { fontWeight: 600, color: "#222" } : undefined}
> >
{char} {char}
@@ -53,7 +56,6 @@ function App() {
const [knownProjects, setKnownProjects] = React.useState<string[]>([]); const [knownProjects, setKnownProjects] = React.useState<string[]>([]);
const [homeDir, setHomeDir] = React.useState<string | null>(null); const [homeDir, setHomeDir] = React.useState<string | null>(null);
const [suggestion, setSuggestion] = React.useState<string | null>(null);
const [suggestionTail, setSuggestionTail] = React.useState(""); const [suggestionTail, setSuggestionTail] = React.useState("");
const [completionError, setCompletionError] = React.useState<string | null>( const [completionError, setCompletionError] = React.useState<string | null>(
null, null,
@@ -99,7 +101,6 @@ function App() {
async function computeSuggestion() { async function computeSuggestion() {
setCompletionError(null); setCompletionError(null);
setSuggestion(null);
setSuggestionTail(""); setSuggestionTail("");
setMatchList([]); setMatchList([]);
setSelectedMatch(0); setSelectedMatch(0);
@@ -174,13 +175,11 @@ function App() {
React.useEffect(() => { React.useEffect(() => {
if (matchList.length === 0) { if (matchList.length === 0) {
setSuggestion(null);
setSuggestionTail(""); setSuggestionTail("");
return; return;
} }
const index = Math.min(selectedMatch, matchList.length - 1); const index = Math.min(selectedMatch, matchList.length - 1);
const next = matchList[index]; const next = matchList[index];
setSuggestion(next.path);
const trimmed = pathInput.trim(); const trimmed = pathInput.trim();
if (next.path.startsWith(trimmed)) { if (next.path.startsWith(trimmed)) {
setSuggestionTail(next.path.slice(trimmed.length)); setSuggestionTail(next.path.slice(trimmed.length));
@@ -247,27 +246,68 @@ function App() {
Recent projects Recent projects
</div> </div>
<ul style={{ listStyle: "none", padding: 0, margin: "8px 0 0" }}> <ul style={{ listStyle: "none", padding: 0, margin: "8px 0 0" }}>
{knownProjects.map((project) => ( {knownProjects.map((project) => {
<li key={project} style={{ marginBottom: "6px" }}> const displayName =
<button project.split("/").filter(Boolean).pop() ?? project;
type="button" return (
onClick={() => void openProject(project)} <li key={project} style={{ marginBottom: "6px" }}>
style={{ <div
width: "100%", style={{
textAlign: "left", display: "flex",
padding: "8px 10px", gap: "6px",
borderRadius: "6px", alignItems: "center",
border: "1px solid #ddd", }}
background: "#f7f7f7", >
cursor: "pointer", <button
fontFamily: "monospace", type="button"
fontSize: "0.9em", onClick={() => void openProject(project)}
}} style={{
> flex: 1,
{project} textAlign: "left",
</button> padding: "8px 10px",
</li> borderRadius: "6px",
))} border: "1px solid #ddd",
background: "#f7f7f7",
cursor: "pointer",
fontFamily: "monospace",
fontSize: "0.9em",
}}
title={project}
>
{displayName}
</button>
<button
type="button"
aria-label={`Forget ${displayName}`}
onClick={() => {
void (async () => {
try {
await api.forgetKnownProject(project);
setKnownProjects((prev) =>
prev.filter((p) => p !== project),
);
} catch (error) {
console.error(error);
}
})();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid #ddd",
background: "#fff",
cursor: "pointer",
fontSize: "1.1em",
lineHeight: 1,
}}
>
×
</button>
</div>
</li>
);
})}
</ul> </ul>
</div> </div>
)} )}
@@ -326,7 +366,6 @@ function App() {
event.preventDefault(); event.preventDefault();
setMatchList([]); setMatchList([]);
setSelectedMatch(0); setSelectedMatch(0);
setSuggestion(null);
setSuggestionTail(""); setSuggestionTail("");
setCompletionError(null); setCompletionError(null);
} else if (event.key === "Enter") { } else if (event.key === "Enter") {

View File

@@ -89,6 +89,13 @@ export const api = {
getKnownProjects(baseUrl?: string) { getKnownProjects(baseUrl?: string) {
return requestJson<string[]>("/projects", {}, baseUrl); return requestJson<string[]>("/projects", {}, baseUrl);
}, },
forgetKnownProject(path: string, baseUrl?: string) {
return requestJson<boolean>(
"/projects/forget",
{ method: "POST", body: JSON.stringify({ path }) },
baseUrl,
);
},
openProject(path: string, baseUrl?: string) { openProject(path: string, baseUrl?: string) {
return requestJson<string>( return requestJson<string>(
"/project", "/project",

View File

@@ -54,4 +54,11 @@ impl ProjectApi {
let projects = fs::get_known_projects(self.ctx.store.as_ref()).map_err(bad_request)?; let projects = fs::get_known_projects(self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(projects)) Ok(Json(projects))
} }
/// Forget a known project path.
#[oai(path = "/projects/forget", method = "post")]
async fn forget_known_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<bool>> {
fs::forget_known_project(payload.0.path, self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(true))
}
} }

View File

@@ -126,6 +126,21 @@ pub fn get_known_projects(store: &dyn StoreOps) -> Result<Vec<String>, String> {
Ok(projects) Ok(projects)
} }
pub fn forget_known_project(path: String, store: &dyn StoreOps) -> Result<(), String> {
let mut known_projects = get_known_projects(store)?;
let original_len = known_projects.len();
known_projects.retain(|p| p != &path);
if known_projects.len() == original_len {
return Ok(());
}
store.set(KEY_KNOWN_PROJECTS, json!(known_projects));
store.save()?;
Ok(())
}
pub fn get_model_preference(store: &dyn StoreOps) -> Result<Option<String>, String> { pub fn get_model_preference(store: &dyn StoreOps) -> Result<Option<String>, String> {
if let Some(model) = store if let Some(model) = store
.get(KEY_SELECTED_MODEL) .get(KEY_SELECTED_MODEL)