summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/contracts/RepositoryPanel.tsx69
-rw-r--r--makima/frontend/src/lib/api.ts74
-rw-r--r--makima/frontend/src/routes/settings.tsx119
3 files changed, 260 insertions, 2 deletions
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<DaemonDirectory[]>([]);
+ // Repository history suggestions
+ const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
+ 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 ? (
<div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3">
- <div className="flex items-center gap-2 mb-2">
+ <div className="flex items-center justify-between mb-2">
<span className="font-mono text-xs text-[#75aafc] uppercase">
Add {sourceTypeLabels[addMode]} Repository
</span>
+ {repoSuggestions.length > 0 && (
+ <button
+ onClick={() => setShowSuggestions(!showSuggestions)}
+ className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors"
+ >
+ {showSuggestions ? "Hide suggestions" : `${repoSuggestions.length} suggestions`}
+ </button>
+ )}
</div>
+ {/* Suggestions dropdown */}
+ {showSuggestions && repoSuggestions.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto">
+ {repoSuggestions.map((suggestion) => (
+ <button
+ key={suggestion.id}
+ onClick={() => applySuggestion(suggestion)}
+ className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] transition-colors border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
+ >
+ <div className="flex items-center justify-between">
+ <span className="text-[#9bc3ff] truncate">{suggestion.name}</span>
+ <span className="text-[10px] text-[#556677]">
+ {suggestion.useCount}×
+ </span>
+ </div>
+ <div className="text-[10px] text-[#556677] truncate">
+ {suggestion.repositoryUrl || suggestion.localPath}
+ </div>
+ </button>
+ ))}
+ </div>
+ )}
+
<input
type="text"
value={name}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 2ea1128..100a85a 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1973,3 +1973,77 @@ export async function answerQuestion(
}
return res.json();
}
+
+// =============================================================================
+// Repository History Types and API
+// =============================================================================
+
+export interface RepositoryHistoryEntry {
+ id: string;
+ ownerId: string;
+ name: string;
+ repositoryUrl: string | null;
+ localPath: string | null;
+ sourceType: "remote" | "local";
+ useCount: number;
+ lastUsedAt: string;
+ createdAt: string;
+}
+
+export interface RepositoryHistoryListResponse {
+ entries: RepositoryHistoryEntry[];
+ total: number;
+}
+
+/**
+ * List all repository history entries.
+ * Returns entries ordered by use_count DESC, last_used_at DESC.
+ */
+export async function listRepositoryHistory(): Promise<RepositoryHistoryListResponse> {
+ 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<RepositoryHistoryListResponse> {
+ 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<void> {
+ 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<string | null>(null);
+ // Repository history state
+ const [repoHistory, setRepoHistory] = useState<RepositoryHistoryEntry[]>([]);
+ const [repoHistoryLoading, setRepoHistoryLoading] = useState(true);
+ const [repoHistoryError, setRepoHistoryError] = useState<string | null>(null);
+ const [deletingRepoId, setDeletingRepoId] = useState<string | null>(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() {
</div>
)}
</section>
+
+ {/* Repository History */}
+ <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
+ <div className="flex items-center justify-between mb-3 pb-2 border-b border-[rgba(117,170,252,0.15)]">
+ <div className="flex items-center gap-2">
+ <h2 className="text-[11px] font-mono uppercase tracking-wide text-[#8899aa]">
+ Repository History
+ </h2>
+ {repoHistory.length > 0 && (
+ <span className="text-[10px] font-mono text-[#556677]">
+ ({repoHistory.length} saved)
+ </span>
+ )}
+ </div>
+ <button
+ onClick={loadRepoHistory}
+ disabled={repoHistoryLoading}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] disabled:opacity-50"
+ title="Refresh"
+ >
+ {repoHistoryLoading ? "..." : "↻"}
+ </button>
+ </div>
+ <p className="text-[#7788aa] font-mono text-[10px] mb-3">
+ Previously used repositories will appear as suggestions when adding repos to contracts.
+ </p>
+
+ {repoHistoryError && <ErrorAlert>{repoHistoryError}</ErrorAlert>}
+
+ {repoHistoryLoading && repoHistory.length === 0 ? (
+ <p className="text-[#7788aa] font-mono text-xs">Loading...</p>
+ ) : repoHistory.length === 0 ? (
+ <div className="text-center py-4">
+ <p className="text-[#7788aa] font-mono text-xs mb-2">No repository history</p>
+ <p className="text-[#556677] font-mono text-[10px]">
+ Add repositories to contracts to build history
+ </p>
+ </div>
+ ) : (
+ <div className="space-y-2 max-h-[300px] overflow-y-auto">
+ {repoHistory.map((entry) => (
+ <div
+ key={entry.id}
+ className="border border-[rgba(117,170,252,0.15)] bg-[#0a1525] p-3"
+ >
+ <div className="flex items-start justify-between gap-2">
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-2 mb-1">
+ <span className="font-mono text-xs text-[#9bc3ff] truncate">
+ {entry.name}
+ </span>
+ <span
+ className={`text-[10px] font-mono uppercase px-1.5 py-0.5 border ${
+ entry.sourceType === "remote"
+ ? "text-cyan-400 border-cyan-700/50 bg-cyan-900/20"
+ : "text-green-400 border-green-700/50 bg-green-900/20"
+ }`}
+ >
+ {entry.sourceType}
+ </span>
+ </div>
+ <div className="font-mono text-[10px] text-[#556677] truncate mb-1">
+ {entry.repositoryUrl || entry.localPath}
+ </div>
+ <div className="flex items-center gap-3 font-mono text-[10px] text-[#7788aa]">
+ <span>Used {entry.useCount}×</span>
+ <span>
+ Last: {new Date(entry.lastUsedAt).toLocaleDateString()}
+ </span>
+ </div>
+ </div>
+ <button
+ onClick={() => handleDeleteRepoHistory(entry.id)}
+ disabled={deletingRepoId === entry.id}
+ className="p-1 font-mono text-[10px] text-[#555] hover:text-red-400 transition-colors disabled:opacity-50"
+ title="Delete from history"
+ >
+ {deletingRepoId === entry.id ? "..." : "×"}
+ </button>
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </section>
</div>
{/* Right Column */}