From 764bd28d08ceaef03cd4050f9568a62d77bbcfca Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 15 Jan 2026 03:37:44 +0000 Subject: Add repository history feature to store and suggest previously used repositories (#18) - Add repository_history table migration with repo_type, repo_path, use_count, last_used_at - Add RepositoryHistoryEntry model and CRUD database functions - Create API endpoints: GET/POST/DELETE /api/v1/repository-history, GET /api/v1/repository-history/suggestions - Update add_remote_repository and add_local_repository handlers to automatically track history - Update frontend API with repository history types and functions - Add Repository History section to Settings page with list of entries and delete functionality - Add suggestions dropdown to RepositoryPanel when entering new repository URL/path - Suggestions filter by repo type (remote vs local) and match on user input Test plan: - Add a remote repository to a contract - verify it appears in Settings history - Add a local repository to a contract - verify it appears in Settings history - Add same repository again - verify use_count increments, not duplicate - When adding new repository, verify suggestions appear based on history - Delete a history entry from Settings - verify it's removed - Verify suggestions only show matching type (remote for remote, local for local) Co-authored-by: Claude Opus 4.5 --- .../src/components/contracts/RepositoryPanel.tsx | 69 +++++++++++- makima/frontend/src/lib/api.ts | 74 +++++++++++++ makima/frontend/src/routes/settings.tsx | 119 +++++++++++++++++++++ 3 files changed, 260 insertions(+), 2 deletions(-) (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/components/contracts/RepositoryPanel.tsx b/makima/frontend/src/components/contracts/RepositoryPanel.tsx index 4170cfb..e314140 100644 --- a/makima/frontend/src/components/contracts/RepositoryPanel.tsx +++ b/makima/frontend/src/components/contracts/RepositoryPanel.tsx @@ -4,8 +4,9 @@ import type { RepositorySourceType, RepositoryStatus, DaemonDirectory, + RepositoryHistoryEntry, } from "../../lib/api"; -import { getDaemonDirectories } from "../../lib/api"; +import { getDaemonDirectories, getRepositorySuggestions } from "../../lib/api"; import { DirectoryInput } from "../mesh/DirectoryInput"; interface RepositoryPanelProps { @@ -53,6 +54,9 @@ export function RepositoryPanel({ const [isPrimary, setIsPrimary] = useState(false); // Daemon directory suggestions for local repositories const [suggestedDirectories, setSuggestedDirectories] = useState([]); + // Repository history suggestions + const [repoSuggestions, setRepoSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); // Fetch daemon directories when "local" mode is selected useEffect(() => { @@ -63,6 +67,36 @@ export function RepositoryPanel({ } }, [addMode]); + // Fetch repository suggestions when mode changes to remote or local + useEffect(() => { + if (addMode === "remote" || addMode === "local") { + getRepositorySuggestions(addMode, undefined, 10) + .then((res) => { + setRepoSuggestions(res.entries); + setShowSuggestions(res.entries.length > 0); + }) + .catch(() => { + setRepoSuggestions([]); + setShowSuggestions(false); + }); + } else { + setRepoSuggestions([]); + setShowSuggestions(false); + } + }, [addMode]); + + // Apply a suggestion to the form + const applySuggestion = (suggestion: RepositoryHistoryEntry) => { + setName(suggestion.name); + if (suggestion.repositoryUrl) { + setUrl(suggestion.repositoryUrl); + } + if (suggestion.localPath) { + setPath(suggestion.localPath); + } + setShowSuggestions(false); + }; + const handleAdd = () => { if (!name.trim()) return; @@ -162,12 +196,43 @@ export function RepositoryPanel({ {/* Add repository form */} {addMode ? (
-
+
Add {sourceTypeLabels[addMode]} Repository + {repoSuggestions.length > 0 && ( + + )}
+ {/* Suggestions dropdown */} + {showSuggestions && repoSuggestions.length > 0 && ( +
+ {repoSuggestions.map((suggestion) => ( + + ))} +
+ )} + { + const res = await authFetch(`${API_BASE}/api/v1/settings/repository-history`); + if (!res.ok) { + throw new Error(`Failed to list repository history: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get repository suggestions based on history. + * Optionally filter by source type and search query. + */ +export async function getRepositorySuggestions( + sourceType?: "remote" | "local", + query?: string, + limit?: number +): Promise { + const params = new URLSearchParams(); + if (sourceType) params.append("source_type", sourceType); + if (query) params.append("query", query); + if (limit) params.append("limit", limit.toString()); + + const queryString = params.toString(); + const url = `${API_BASE}/api/v1/settings/repository-history/suggestions${ + queryString ? `?${queryString}` : "" + }`; + + const res = await authFetch(url); + if (!res.ok) { + throw new Error(`Failed to get repository suggestions: ${res.statusText}`); + } + return res.json(); +} + +/** + * Delete a repository history entry. + */ +export async function deleteRepositoryHistory(id: string): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/settings/repository-history/${id}`, + { + method: "DELETE", + } + ); + if (!res.ok) { + throw new Error(`Failed to delete repository history: ${res.statusText}`); + } +} diff --git a/makima/frontend/src/routes/settings.tsx b/makima/frontend/src/routes/settings.tsx index 7ca40ba..d3f4c1b 100644 --- a/makima/frontend/src/routes/settings.tsx +++ b/makima/frontend/src/routes/settings.tsx @@ -11,9 +11,12 @@ import { changeEmail, deleteAccount, listDaemons, + listRepositoryHistory, + deleteRepositoryHistory, type ApiKeyInfo, type CreateApiKeyResponse, type Daemon, + type RepositoryHistoryEntry, } from "../lib/api"; // ============================================================================= @@ -304,9 +307,16 @@ export default function SettingsPage() { const [daemonsLoading, setDaemonsLoading] = useState(true); const [daemonsError, setDaemonsError] = useState(null); + // Repository history state + const [repoHistory, setRepoHistory] = useState([]); + const [repoHistoryLoading, setRepoHistoryLoading] = useState(true); + const [repoHistoryError, setRepoHistoryError] = useState(null); + const [deletingRepoId, setDeletingRepoId] = useState(null); + useEffect(() => { loadApiKey(); loadDaemons(); + loadRepoHistory(); }, []); // Auto-refresh daemons every 30 seconds @@ -342,6 +352,30 @@ export default function SettingsPage() { } }; + const loadRepoHistory = async () => { + try { + setRepoHistoryError(null); + const response = await listRepositoryHistory(); + setRepoHistory(response.entries); + } catch (err) { + setRepoHistoryError(err instanceof Error ? err.message : "Failed to load repository history"); + } finally { + setRepoHistoryLoading(false); + } + }; + + const handleDeleteRepoHistory = async (id: string) => { + try { + setDeletingRepoId(id); + await deleteRepositoryHistory(id); + setRepoHistory((prev) => prev.filter((entry) => entry.id !== id)); + } catch (err) { + setRepoHistoryError(err instanceof Error ? err.message : "Failed to delete entry"); + } finally { + setDeletingRepoId(null); + } + }; + const handleCreate = async () => { try { setActionLoading(true); @@ -692,6 +726,91 @@ export default function SettingsPage() {
)} + + {/* Repository History */} +
+
+
+

+ Repository History +

+ {repoHistory.length > 0 && ( + + ({repoHistory.length} saved) + + )} +
+ +
+

+ Previously used repositories will appear as suggestions when adding repos to contracts. +

+ + {repoHistoryError && {repoHistoryError}} + + {repoHistoryLoading && repoHistory.length === 0 ? ( +

Loading...

+ ) : repoHistory.length === 0 ? ( +
+

No repository history

+

+ Add repositories to contracts to build history +

+
+ ) : ( +
+ {repoHistory.map((entry) => ( +
+
+
+
+ + {entry.name} + + + {entry.sourceType} + +
+
+ {entry.repositoryUrl || entry.localPath} +
+
+ Used {entry.useCount}× + + Last: {new Date(entry.lastUsedAt).toLocaleDateString()} + +
+
+ +
+
+ ))} +
+ )} +
{/* Right Column */} -- cgit v1.2.3