summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/routes/document-directives.tsx96
1 files changed, 69 insertions, 27 deletions
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 801e397..49b9fd0 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -13,6 +13,7 @@ import {
type Task,
type DirectiveContractTasksResponse as ContractTasksResponse,
type DirectiveContractMergeMode as ContractMergeMode,
+ type RepositoryHistoryEntry,
listDirectiveContracts as listContracts,
createDirectiveContract as createContract,
getDirectiveContract as getContract,
@@ -34,6 +35,7 @@ import {
pickUpOrders,
stopTask,
skipDirectiveStep,
+ getRepositorySuggestions,
} from "../lib/api";
import { SidebarContextMenu, type ContextMenuItem } from "../components/SidebarContextMenu";
import { TaskPage } from "../components/directives/TaskPage";
@@ -1678,10 +1680,9 @@ export default function DocumentDirectivesPage() {
const [newEphemeralFor, setNewEphemeralFor] = useState<DirectiveSummary | null>(null);
const handleSubmitNewContract = useCallback(
- async (title: string, body: string, repositoryUrl: string) => {
+ async (title: string, repositoryUrl: string) => {
const d = await createDirective({
title,
- contractBody: body,
repositoryUrl: repositoryUrl.length > 0 ? repositoryUrl : undefined,
});
setShowNewContract(false);
@@ -1970,21 +1971,24 @@ export default function DocumentDirectivesPage() {
/**
* Modal for creating a new directive (= "contract" in the doc-mode UI).
- * Title + goal are required; repository_url is optional. On submit calls
- * useDirectives.create and navigates the user into the new directive.
+ * Title is required; repository_url and the contract body are optional
+ * and can be edited after creation in the document editor. On submit
+ * calls useDirectives.create and navigates the user into the new
+ * directive.
*/
function NewContractModal({
onClose,
onSubmit,
}: {
onClose: () => void;
- onSubmit: (title: string, goal: string, repositoryUrl: string) => Promise<void>;
+ onSubmit: (title: string, repositoryUrl: string) => Promise<void>;
}) {
const [title, setTitle] = useState("");
- const [goal, setGoal] = useState("");
const [repo, setRepo] = useState("");
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState<string | null>(null);
+ const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
+ const [showRepoSuggestions, setShowRepoSuggestions] = useState(false);
const titleRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -1996,15 +2000,40 @@ function NewContractModal({
return () => document.removeEventListener("keydown", onKey);
}, [onClose]);
+ // Load recent remote-repository suggestions on mount. Swallow errors
+ // — the suggestions list is purely a convenience and should never
+ // block contract creation.
+ useEffect(() => {
+ let cancelled = false;
+ getRepositorySuggestions("remote", undefined, 10)
+ .then((res) => {
+ if (cancelled) return;
+ setRepoSuggestions(res.entries);
+ setShowRepoSuggestions(res.entries.length > 0);
+ })
+ .catch(() => {
+ if (cancelled) return;
+ setRepoSuggestions([]);
+ setShowRepoSuggestions(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => {
+ setRepo(suggestion.repositoryUrl ?? "");
+ setShowRepoSuggestions(false);
+ }, []);
+
const submit = async (e: React.FormEvent) => {
e.preventDefault();
const t = title.trim();
- const g = goal.trim();
- if (!t || !g || submitting) return;
+ if (!t || submitting) return;
setSubmitting(true);
setErr(null);
try {
- await onSubmit(t, g, repo.trim());
+ await onSubmit(t, repo.trim());
} catch (caught) {
setErr(caught instanceof Error ? caught.message : String(caught));
} finally {
@@ -2043,32 +2072,45 @@ function NewContractModal({
</div>
<div className="space-y-1">
<label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
- Goal
- </label>
- <textarea
- value={goal}
- onChange={(e) => setGoal(e.target.value)}
- onKeyDown={(e) => {
- if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
- void submit(e as unknown as React.FormEvent);
- }
- }}
- rows={4}
- placeholder="Describe what the contract should achieve"
- className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none"
- />
- </div>
- <div className="space-y-1">
- <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
Repository URL (optional)
</label>
<input
type="text"
value={repo}
onChange={(e) => setRepo(e.target.value)}
+ onFocus={() => {
+ if (repoSuggestions.length > 0) setShowRepoSuggestions(true);
+ }}
placeholder="e.g. https://github.com/owner/repo"
className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
/>
+ {showRepoSuggestions && repoSuggestions.length > 0 && (
+ <div className="space-y-1 pt-1">
+ <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
+ Recent Repositories
+ </label>
+ <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}
+ type="button"
+ onClick={() => applyRepoSuggestion(suggestion)}
+ className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] 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] ml-2">
+ {suggestion.useCount}×
+ </span>
+ </div>
+ <div className="text-[10px] text-[#556677] truncate">
+ {suggestion.repositoryUrl}
+ </div>
+ </button>
+ ))}
+ </div>
+ </div>
+ )}
</div>
{err && (
<p className="text-[11px] font-mono text-red-400">{err}</p>
@@ -2084,7 +2126,7 @@ function NewContractModal({
</button>
<button
type="submit"
- disabled={!title.trim() || !goal.trim() || submitting}
+ disabled={!title.trim() || submitting}
className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed"
>
{submitting ? "Creating…" : "Create contract"}